At authenticator enrolment, what does the server generate, show once, and require before it commits the enrolment?
answer
- two steps, not one
- pending until a code comes back
- shown once, no read path after
- the pending row expires on its own
- activation is what starts demanding codes
basics
~20 sThe server draws a fresh shared secret, shows it exactly once as a scannable image and as typeable text, holds it in a pending enrolment row, and activates it only after one correct code proves the authenticator really has it.
solid answer
~40 sEnrolment is two requests, not one. The prepare step generates a fresh shared secret from a cryptographically secure generator, stores it encrypted on a **pending** enrolment row with its own short expiry, and renders it in that one response - as a provisioning image to scan and as the same value in typeable form for a handset whose camera will not cooperate. It is never shown again. The confirm step takes one code the holder reads off the app, verifies it against the pending secret, and only then flips the row to active. That ordering is the whole point: without it a scan that silently failed still switches the account to demanding codes, and the next sign-in locks out a holder who is standing in a shed with a movement to record.
code
pseudocode · 27 lines# step 1 - prepare (signed-in holder, nothing active yet)
secret = random_secret_from_csprng()
pending = enrolments.replace_pending(
holder_id = current_holder.id,
state = "PENDING",
secret_ct = encrypt(secret, key_id = current_secret_key_id()),
expires_at = now() + 10.minutes)
render_once(secret) # scannable image + typeable text, this response only
# step 2 - confirm (holder reads one code off the authenticator app)
p = enrolments.find_pending(current_holder.id)
if p == NONE:
fail("no enrolment in progress")
if now() > p.expires_at:
p.delete()
fail("enrolment expired, start again")
if not code_matches_within_window(decrypt(p.secret_ct), submitted_code, now()):
fail("that code did not match") # p survives - the holder may retry
p.state = "ACTIVE" # only now does sign-in demand a code
p.activated_at = now()
p.save()
issue_backup_codes(current_holder) # displayed once, in this responsego deeper
Recall the two steps and the order: the server generates and shows the shared secret once, and the account only starts demanding codes after one correct code has come back from the app.
Design the record. Describe the pending row, its own short expiry, what a wrong confirmation code does and does not destroy, and why the secret has no read path once the prepare response has been sent.
Reason about the field. Name the failures confirm-before-commit removes - a silent scan failure, a hand-keyed typo, an abandoned page - and say what each would have cost the holder at the next sign-in.
Treat it as rollout risk. A mandatory factor enrolled without a confirm step converts a support ticket into an outage for the people who least expect one, and the recovery path you build is priced by that mistake.
## Enrolment is two requests, not one The thing that separates a working second-factor rollout from a support queue is that enrolment has a **prepare** step and a **confirm** step, and the account's behaviour changes only at the second one. The unit being created is an **enrolment row** owned by one holder of a movement licence. It has a state, a shared secret, and a clock of its own. Everything below is about what goes on that row and when. ## The prepare step: generate, store pending, show once On prepare the server: - draws a **fresh shared secret** from a cryptographically secure generator - never derived from the account, never reused from a previous enrolment; - writes it, encrypted, onto a row whose state is `PENDING` and which is bound to the currently signed-in holder; - stamps that row with a short expiry of its own, measured in minutes; - renders the secret **in that response and only that response**, in two forms: a provisioning image the authenticator app can scan, and the same value as typeable text. The two display forms are not a nicety. Scanning is the step that fails in the field: a cracked camera lens, a shed with no light, a handset that will not focus on a screen in sunlight. The typeable form keeps those holders inside the same single-display rule instead of driving them to photograph the screen and mail it to themselves. After this response the secret has **no read path out of the system**. There is no settings page that shows it again, no support tool that prints it. Moving to a new handset means a new enrolment, not a re-display. ## The confirm step is the whole point Confirmation takes one code the holder reads off the app and verifies it against the pending secret. Only if it matches does the row become `ACTIVE`, and only then does sign-in start demanding a second factor. That ordering removes a specific family of failures, all of which look identical to the server if the enrolment is committed at display time: 1. **The scan silently failed.** The app shows nothing, or shows an entry for a different service the holder already had. 2. **The secret was typed in wrong.** A long string keyed by hand in a vehicle cab is exactly where a transcription error hides. 3. **The holder abandoned the page** after the secret was displayed but before touching the app at all. 4. **The app was uninstalled or reset** between the scan and anything being written down. 5. **The handset's clock is so far out** that nothing it produces will be accepted, which is worth discovering now rather than at the next sign-in. In every one of those, committing at display time produces the same outcome: the account now demands a code that nobody in the world can produce, and the holder discovers it at six in the morning with a lorry waiting. Confirm-before-commit converts all five into a harmless retry. | | pending enrolment | active enrolment | |---|---|---| | created by | the prepare step | a successful confirm | | secret visible to the holder | once, in the prepare response | never | | does sign-in demand a code | no | yes | | survives a wrong code | yes, retry until it expires | not applicable | | removed by | its own expiry, or a newer prepare | an explicit removal or replacement | ## The pending row has its own clock A pending enrolment is a loose end, so it expires on its own: minutes, not days. A wrong confirmation code does **not** delete it - a single mistyped digit should cost a retry, not the whole ceremony - but its expiry does, and starting a new prepare supersedes it. Only one pending enrolment per holder should exist at a time. Two live pending secrets mean a confirmation code that could match either, and no way to say afterwards which handset the account now depends on. Where an enrolment is already active, it keeps working untouched until a new one is confirmed, so a failed attempt to move handsets never leaves the account with nothing. ## What activation turns on Activation is the commit point, and it is where the rest of the second-factor machinery starts: - the account begins demanding a code on the paths that require the factor; - the set of **backup codes** is issued and displayed, once, in the activation response - issuing them earlier is meaningless, because there may never be an enrolment for them to back up; - the holder is told, in plain terms, that the secret cannot be shown again and that a new handset means a new enrolment. What the server ends up holding is small and completely describable: one row per enrolment, carrying the state, the encrypted secret and its key identifier, the activation time, and the bookkeeping the verifier needs afterwards.
- What should happen if a holder starts a second enrolment while a pending one is still open?The newer attempt supersedes the older one: replace the pending row rather than keeping two candidates alive. Two pending secrets mean a confirmation code that may match either, and no way to say afterwards which handset the account depends on. Where an enrolment is already active it keeps working untouched until the new one is confirmed.
- Why show the secret as typeable text as well as a scannable image?Because scanning is the step that fails in the field - a cracked lens, a dark shed, a screen the camera will not focus on. The typeable form keeps that holder inside the same show-once rule instead of photographing the screen. It also makes the confirm step earn its keep, since a hand-keyed string is where transcription errors hide.
- Should a failed confirmation code delete the pending enrolment?No. A mistyped digit should cost a retry, not the ceremony, so the pending row survives a mismatch and is removed only by its own short expiry or by a newer prepare. What must not survive is the display: the secret is rendered in the prepare response alone, so a retry means retyping the code, never re-showing the secret.
saying these in an interview costs you the question
- Activates the enrolment as soon as the secret is displayed
- Lets the holder re-display the shared secret later from account settings
- Deletes the pending enrolment on the first mistyped confirmation code
- Keeps a pending enrolment alive indefinitely with no expiry
- Assumes scanning the image proves the authenticator stored it
- Issues backup codes before any code has been verified