What happens to a pre-authentication handle when a half-finished login is abandoned, when the factor is requested again, and when it finally succeeds?
answer
- one handle, one attempt
- the deadline never moves
- re-request keeps the same handle
- consume, then issue fresh
- promotion is the trap
basics
~20 sAbandonment ends the attempt when the handle's short absolute clock passes, and the visitor restarts from the password step. Requesting the factor again keeps the same handle and the same deadline. Success consumes it: destroy the handle, then issue a fresh session.
solid answer
~40 sTreat the handle as the identity of one attempt, with three exits. **Abandoned**: the absolute clock passes, the record is deleted, and a returning visitor starts at the password step again — nothing about the earlier password proof is durable. **Re-requested**: the attempt has not changed, so the same handle stays and its deadline does not move; a counter on the record caps how many times the factor may be re-requested within the attempt. **Succeeded**: the handle is *consumed* — read and deleted in one step — and a brand-new session identifier is issued for the authenticated session. Promotion is the trap: upgrading the record in place leaves a value that existed before full authentication still valid after it, and it is the one value an attacker in the flow already knows.
code
pseudocode · 19 lineson second_factor_verified(handle_id, claimed_account):
record = handle_store.consume(handle_id) # single read-and-delete
if record is missing:
return unauthenticated() # expired, abandoned, or already used
if record.account != claimed_account:
return unauthenticated()
session = session_store.create(subject = record.account) # brand-new identifier
audit_log.write("login.completed", record.account, record.started_at)
return set_session_cookie(session.id)
on factor_requested_again(handle_id):
record = handle_store.get(handle_id)
if record is missing:
return unauthenticated()
record.resends = record.resends + 1
if record.resends > RESEND_CAP:
handle_store.delete(handle_id) # attempt is over, not merely refused
return attempt_ended()
handle_store.put(handle_id, record) # same handle, same started_atgo deeper
Know that an unfinished login expires on its own and that coming back later means starting from the password step again. The handle is not something a visitor keeps.
Explain the mechanics of each exit: what deletes the record, why re-requesting the factor does not move the deadline, and why the handle is read and deleted in one step at success.
Show you have operated it: name the symptoms of a refreshed-by-activity handle, a promoted handle that still opens the factor endpoint, and a double-verification caused by reading rather than consuming.
Weigh the cost of a strict attempt window against the support load of visitors who genuinely need longer, and decide where that trade sits for a shared physical terminal versus a personal device.
## One handle, one attempt, one clock The pre-authentication handle issued after the password step is not a small session; it is the **identity of one login attempt**. That framing answers all three exits, because everything that happens between the password step and the second factor is an event on the same attempt, and the attempt either ends or it does not. Its record carries the provisionally-matched account, which factor is pending, how many verification attempts and how many re-requests have been spent, and the instant the attempt began. That last field is the only clock. A handle that is refreshed by activity can be held open on the counter's shared terminal by anything that keeps making requests, which is precisely the situation a short attempt window exists to prevent. ## Abandoned: the visitor walks away A camper is called away mid-login and comes back an hour later. The expected behaviour is the plain one: the handle is gone, and they begin again at the password step. Two things are worth saying about why: - **The password proof is not durable.** Accepting only the second factor on return would mean the first factor's result outlived the attempt it belonged to — and at a counter where several people use one terminal across a shift, the person who returns to the browser is not reliably the person who typed the password. - **Deletion is not only expiry.** Expiry frees the record; an explicit abandon endpoint (a visible cancel, or starting a new password step) should delete it immediately so an abandoned attempt is not still accepting a code minutes later. ## Re-requested: same attempt, same handle When the visitor asks for the factor to be sent again, the attempt has not restarted — they are still the same person at the same window trying to finish the same login. So: 1. the handle stays the same, which keeps any page the visitor already has open working; 2. the deadline does **not** move, so re-requesting cannot extend the window; 3. a counter on the record caps how many re-requests one attempt may spend, and exhausting it ends the attempt rather than merely refusing the request. Replacing the handle on every re-request buys nothing and costs something: it breaks the tab the visitor is looking at, and it produces a trail of orphan records that all still name the same account. What replaces the delivered proof itself, and how often it may be requested, is a separate concern from the handle that ties the attempt together. ## Succeeded: consume, do not promote At success two values must exist for an instant and then one must be gone: | Step | What happens | Why | |---|---|---| | 1 | The handle is **consumed** — read and deleted atomically | A second request replaying the same handle finds nothing, so the verification cannot run twice | | 2 | The record's account is checked against the account the request claims | A handle from one attempt must not finish a different one | | 3 | A **new** session identifier is issued for the authenticated session | The identifier is re-issued at authentication as a matter of course; the handle is a second pre-authentication value that must not be the thing upgraded | | 4 | The attempt record is deleted; what needs keeping goes to the log | A live credential is a poor audit trail | Step 3 is the one candidates miss. Teams that carefully re-issue the session identifier at login sometimes *promote the handle into it* — same value, new meaning — which reintroduces exactly the problem the re-issue was there to avoid, because the promoted value is the one value that was already in play before the account was proven. ## What it looks like when it is wrong - Codes accepted for attempts that ended twenty minutes ago — the handle is being refreshed by activity. - A visitor completing a login on a terminal where a colleague typed the password — the attempt outlived the human. - Two reservations created from one verification — the handle was read rather than consumed, and a retry ran the handler twice. - A handle that still opens the factor endpoint after sign-in — it was promoted instead of destroyed. ## Answering it in an interview Say "one handle, one attempt, one clock" and then walk the three exits. The sentence that earns the question is the last one: at success the handle is consumed and a fresh session identifier is issued, because a value that existed before the account was proven must not be the value that carries authority afterwards.
- Why check the record's account against the account the verification request claims, when the handle already names it?Because the request and the handle are two separate inputs, and an endpoint that trusts only one of them will happily finish an attempt for a different account if the other is attacker-supplied. The check costs a comparison and removes a whole class of mix-up between concurrent attempts on the same machine.
- The visitor starts a second login in a new tab while the first is still pending. What should happen?The new password step issues a new handle and the previous attempt's record is deleted, because the first attempt is no longer the one the human is working on. Leaving both live means two accepted paths to finish a login, each with its own counters, and whichever is attacked is the weaker one.
- What should be logged about an attempt that expires without ever completing?The attempt's start and end, the account it provisionally matched, how it ended and how many re-requests it spent — as a log line, not by leaving the record alive. Abandoned attempts clustering on one account or one terminal is a signal worth having, and it costs nothing once the record is already being deleted.
saying these in an interview costs you the question
- Refresh the handle on every request so the visitor is not rushed.
- On return, skip the password step since it already passed.
- Upgrade the pending record in place once the code checks out.
- Issue a new handle on every re-request to be safe.
- Keep the handle alive after success so retries can find the attempt.