Authorization code flow moves a short-lived code through the browser, then exchanges it at the token endpoint. PKCE binds that code to a random verifier held by the initiating client. A separate state value binds the redirect response to the login attempt in the current browser session. These controls answer different questions. Neither proves that a returned identity token is valid or that the user can access a local case. For sign-in, use the identity layer above the authorization flow and keep provider configuration fixed on the server. A callback is a new trust boundary, even when it comes from a familiar host.
Authorization Code, PKCE, and Callback Binding
Working case
The permit portal offers sign-in from two organizations. Reviewer 47 starts at organization 6, opens a second tab for organization 9, and receives callbacks in reverse order. A single global pending-login record would let the later tab consume the earlier state or exchange its code against the wrong issuer. Store each short-lived attempt under its own opaque state, with issuer, redirect URI, verifier, nonce, creation time, and initiating browser-session identity. A callback must match and consume exactly one attempt. If reviewer 47 refreshes a used callback URL, it fails cleanly instead of creating a second app session.
Implementation boundary
function mayExchangeCallback(callback, attempt, browserSessionId, now) {
return Boolean(attempt) && !attempt.consumed &&
callback.state === attempt.state &&
browserSessionId === attempt.browserSessionId && now < attempt.expiresAt;
}
console.log(mayExchangeCallback({ state: "state-47" }, { state: "state-47", browserSessionId: "session-6", consumed: false, expiresAt: 90 }, "session-9", 63));
// Output: falseGenerate a high-entropy verifier and state using a cryptographic random source. Send the S256 challenge in the authorization request and retain the verifier in server-held, short-lived attempt state where the architecture permits. Bind the attempt to the same browser session that initiated it. On callback, reject missing, expired, consumed, or mismatched state before exchanging the code. Select the configured token endpoint from the stored issuer, never from a callback parameter. Use the exact registered redirect URI and client configuration. Protect the attempt store against concurrent consumption with an atomic delete or compare-and-swap. Handle error callbacks without echoing codes or tokens into logs, analytics, or user-visible text. After exchange, verify the identity token separately before creating a local session.
Cost and boundaries
One attempt record adds O(1) server storage and a short expiry; an atomic consume adds a storage round trip. Multiple pending tabs require one record per attempt rather than a single cookie slot. The token exchange adds network latency and can fail independently of the authorization redirect. Retrying a used code is generally not a recovery strategy. Measure callback rejection categories without storing the raw code or verifier, plus issuer mismatch, state expiry, and token-endpoint latency. Remove abandoned attempts on expiry. A server session may cost storage, but it also gives the application a place to revoke and audit access after the external exchange.
Failure trace
Replay the same callback twice and confirm only the first attempt can consume state. Start two provider attempts in separate tabs, complete them out of order, and verify each binds to its own issuer and verifier. Replace the returned state, omit it, or age the attempt past its deadline; no token exchange should occur. Swap the configured redirect URI or send a code from a different issuer and reject the exchange. A successful HTTP response from the token endpoint is not enough: a malformed or wrongly addressed identity token must still fail the next boundary.
Verification
- State and verifier have separate purposes and are generated afresh.
- A callback consumes one matching browser-session attempt.
- Issuer and redirect configuration come from trusted attempt state.
Practice drill
Create 47 pending attempts across two issuer configurations, each with a random state and verifier. Complete one normal callback, one replay, one expired callback, and two out-of-order tab callbacks. Count exchanges and assert that only valid, unconsumed attempts reached the token endpoint. Seed logs with a test code and verifier, run the flow, then search logs and traces to prove neither value was copied. Document which process owns expiry and what the user sees when an old tab returns.
Decision note
A callback belongs to one initiating attempt; successful exchange alone does not establish a local identity.
Common Mistakes
- Reusing one pending-login slot across tabs.
- Treating PKCE as a substitute for state or identity-token verification.
- Logging the code, verifier, or full callback URL.
Related lessons
Federated Identity and Session Lifecycle; ID Token Verification and Stable Account Identity; Refresh Token Rotation and App Session Boundary; Federated Account Linking, Logout, and Revocation; Federated Login Callback Boundary; Identity and Application Security.
Apply and check
Build Project: federated permit reviewer sign-in and review Web Development: federated identity and session contracts quiz.
