What handle should a server issue between a successful password step and a verified second factor, and how must it differ from a signed-in session?
answer
- neither anonymous nor authenticated
- one attempt, one pending factor
- carries no authority at all
- a flag makes rejection opt-in
- claimed account stays server-side
basics
~20 sA short-lived, single-purpose pre-authentication handle bound to one login attempt and to the factor still pending, carrying no authority to reach application endpoints. It must be its own credential, not a signed-in session record wearing a pending flag.
solid answer
~50 sBetween the two steps the caller is neither anonymous nor authenticated, so the server issues a distinct **pre-authentication handle**: an opaque value, carried like any session reference, whose server-side record holds only the account the password step provisionally matched, which factor is pending, the attempts spent and the moment the attempt began. It runs on one short absolute clock, and the only endpoints that accept it are the ones that finish or abandon that attempt. The account identifier stays server-side so no later code can read a user out of it. The alternative — issuing the real session and setting a `pendingSecondFactor` attribute on it — inverts the polarity: rejecting that session becomes a rule every endpoint must remember, and endpoints added later do not. With a separate handle, an application endpoint sees no session at all and answers `401 Unauthorized` without anyone having remembered anything.
code
http · 18 linesPOST /login HTTP/1.1
Host: reservations.example
Content-Type: application/x-www-form-urlencoded
username=r.okafor&password=...
HTTP/1.1 200 OK
Set-Cookie: pending_login=9f2c7a...; Path=/login/factor; Max-Age=300; HttpOnly; Secure
Content-Type: application/json
{"next":"second-factor","pending":"code-to-phone"}
GET /reservations/mine HTTP/1.1
Host: reservations.example
Cookie: pending_login=9f2c7a...
HTTP/1.1 401 Unauthorizedgo deeper
Recall that a login with two steps has a middle state, and that the value handed back after the password step is not a sign-in. Being able to say the caller is 'not signed in yet' is most of the answer at this level.
Explain the mechanics: what the handle's server-side record holds, why the claimed account stays out of the client's copy, which endpoints accept it, and the single absolute clock it runs on.
Demonstrate the polarity argument — a flag makes rejection a rule every endpoint must remember and a separate credential makes access something an endpoint must be granted — and name the kinds of endpoints that get added later and quietly inherit the flag.
Frame it as where a safety property is stored: in an attribute that all future code must consult, or in the shape of the credential itself. Choosing the second is what keeps the property true for code nobody has written yet.
## Three states, not two Most session code models two states — anonymous, and signed in — and a two-step login introduces a third that lasts seconds to minutes. At a national park's campsite reservation counter the sequence is: the visitor types a password, the server matches it against an account, and the server then wants a second proof before it will act as that account. For the length of that gap the caller is **neither anonymous nor authenticated**. They are one specific, provisionally-identified attempt in progress. Every request inside that gap has to be tied back to the same attempt, and the transport gives the server nothing to do that with, so the server issues a value: the **pre-authentication handle**. Mechanically it looks like any other session reference — an opaque, unguessable value in a cookie marked `HttpOnly` and `Secure`, or returned in the response body for a caller that is not a browser. Semantically it is a different object from a signed-in session, and that difference is the whole question. ## The handle against the session it is not | Property | Pre-authentication handle | Signed-in session | |---|---|---| | Lifetime | one absolute clock, minutes | an idle clock and an absolute clock, hours or days | | Accepted by | the endpoints that finish or abandon the attempt | the application | | Bound to | one login attempt and the factor pending on it | the account | | Authority | none; it authorizes no business action | the account's own authority | | When the attempt ends | destroyed, in either direction | continues until it expires or is ended | ## What the server stores behind it - the account the password step **provisionally** matched — server-side, not in the value the client holds; - which factor is pending, because the endpoints that accept the handle differ per factor; - how many verification attempts have been spent against this attempt; - when the attempt started, which is the only clock the handle runs on. Keeping the account identifier server-side matters more than it looks. The moment the claimed user is readable out of the value the client carries, some later code path reads it and treats it as the current user, and the distinction the handle exists to draw is gone. ## The failure it prevents The shortcut every team is tempted by is to authenticate at the password step, issue the ordinary session, and set an attribute on it — `pendingSecondFactor = true` — that the rest of the application is expected to consult. It works perfectly on the day it is written. The problem is its **polarity**: 1. With a flag, *signed in* is the default and *reject the half-proven caller* is a rule that every endpoint must remember to apply. 2. With a separate handle, *not signed in* is the default and reaching the application requires a session the caller does not yet have. The first shape fails open, and it fails open silently and later: a report export added six months on, an internal route another team adds, a scheduled action triggered by a request. None of those are wrong about the session — the session genuinely says authenticated. They are wrong because the truth was stored in an attribute rather than in the shape of the credential. With a distinct handle, a request that presents only the handle to an application endpoint carries no session at all. The ordinary authentication check fails on its own and the endpoint answers `401 Unauthorized`, which is the correct code here: the server does not yet know who this is. It is not a refusal of a known caller. ## The honest edges - **Per attempt, not per visitor.** Starting the password step again issues a new handle and abandons the previous record, so two tabs cannot half-authenticate into each other. - **Not extended by use.** Requests against the handle do not push its deadline out; an attempt that can be held open by activity is an attempt that can be parked indefinitely on a shared counter terminal. - **Single purpose.** The endpoints that verify the pending factor, request it again, or abandon the attempt accept the handle; nothing else does. A cookie `Path` scoped to those endpoints is a cheap way to keep the browser from sending it anywhere else, though the server still has to enforce it. - **Destroyed when the attempt ends**, whichever way it ends — verified, abandoned, or expired. - **Non-browser callers** get the same value in a response body and send it back in a request header. The reasoning is unchanged; only the carrier moves. ## Answering it in an interview Name the three states out loud, because most answers only have two. Say what the handle carries — nothing — and what its record holds. Then say the polarity sentence: a flag on a signed-in session makes rejection opt-in, and a separate credential makes access opt-in. That sentence is what the interviewer is listening for.
- Why keep the provisionally-matched account identifier out of the value the client holds?Because anything readable there eventually gets read. If the claimed account travels in the handle, a later code path will treat it as the current user without asking whether the second factor ever landed. Keeping it in the server-side record means the only way to learn who the caller is, is to look up a record that also says the attempt is unfinished.
- Does the same reasoning apply when the second step is a device the visitor already carries rather than a delivered code?Yes — the handle exists because the attempt is unfinished, not because of what finishes it. Whatever the pending factor is, the server still needs one value tying the next request to this attempt, still stores which factor is pending, and still destroys the handle when the attempt ends. Only the endpoints that accept it change.
- The service also serves callers that are not browsers. Does the handle change shape?Only its carrier. The value comes back in the response body and the caller returns it in a request header instead of a cookie. Its lifetime, its single purpose, its server-side record and its destruction at the end of the attempt are identical, because those properties come from what the handle means, not from how it travels.
It is the numbered queue ticket at the counter, not the reservation receipt. The ticket proves you are mid-transaction at window three; nobody hands you a pitch key for it. Stamping the ticket to turn it into the receipt is how a half-served visitor walks out with a key.
saying these in an interview costs you the question
- Just set a pendingSecondFactor flag on the signed-in session.
- The handle can live as long as the session it will become.
- Every endpoint will obviously check the pending flag.
- Put the matched account id in the handle so the page can greet the user.
- Reuse one handle for all of a visitor's login attempts.
- Give the handle read-only access so the page can preload the account's reservations.