Helpful first: Who holds the ticket: OAuth 2.0 roles and bearer token verification
Key points
- Browser code delivery, client response checks, server grant validation, and token receipt are separate steps.
- Explain how
state,iss(RFC 9207),redirect_, and PKCEuri code_(verifier V7) each close a distinct attack vector
Why tokens do not belong in browser redirect URLs
When a user signs in through a browser redirect, every query parameter on that redirect URL travels through the front channel: browser history, operating-system URL handlers, corporate proxy logs, and potentially Referer headers. If an Authorization Server placed the live access token directly in the redirect URL, any local log reader or history leak would capture a working bearer credential.
To prevent front-channel token leakage, OAuth 2.0 Security Best Current Practice (RFC 9700) mandates the Authorization Code flow with PKCE (RFC 7636): browser redirects carry only a short-lived, single-use authorization code (C61), while the actual access token (AT61) is returned only over an authenticated server-to-server back-channel POST response.
| Protocol Parameter | Checked By | When Checked | Attack Prevented |
|---|---|---|---|
state (high-entropy random) | Confidential Web Client | Step 3: upon receiving browser callback before calling / | Cross-Site Request Forgery (CSRF) login/account-linking fixation on the callback endpoint |
iss (RFC 9207 issuer) | Confidential Web Client | Step 3: alongside state on the callback query string | Authorization Server Mix-Up attacks in multi-IdP environments |
redirect_ exact match | Authorization Server | Step 1 (authorize) & Step 5 (token redemption) | Redirecting codes to attacker-controlled URIs or open redirectors |
PKCE (code_ / code_ V7) | Authorization Server | Step 5: hashes incoming V7 and compares to stored challenge for C61 | Authorization code interception and injection attacks |
Step-by-step trace: callback checks, back-channel POST, and refusal variants
Step through the eight stages of the bounded confidential-client flow in the figure below. First, the confidential web client generates a high-entropy state value and a local PKCE code_ (V7), keeping V7 in local memory while sending only its hash (code_) in the authorization request. After the user approves at the expected HTTPS issuer, the Authorization Server redirects the browser back to the client’s configured HTTPS callback with one-time code C61, state, and iss (RFC 9207). Notice that callback arrival does not imply acceptance: in Step 3 the client verifies State: valid and exact expected Issuer: valid before making any back-channel call.
Next, in Step 4 and Step 5, the client sends an authenticated HTTPS back-channel POST containing client credentials, C61, the redemption redirect_, and the secret code_ (V7). Both front-channel and back-channel links use authenticated HTTPS so a passive cafe Wi-Fi observer never reads plaintext parameters. Only after the Authorization Server verifies Auth: valid, Code: valid (C61 single-use consumption), URI: match, and PKCE: match does it issue AT61 (Issued: AT61) and deliver Token: AT61 in Step 7. RFC 9700 says clients SHOULD NOT use the implicit grant unless its stated attack mitigations are met, and MUST NOT use the resource owner password credentials grant. Authorization Code is the recommended flow here. Compare the two refusal variants: if the client sends a wrong PKCE verifier (V0 instead of V7), or if the redemption redirect_ mismatches the callback URI (URI: different, so PKCE: not run), the server returns HTTP 400 invalid_ (error=invalid_) and issues Token: none. Failed-redemption code lifetime is outside this example; do not assume the failed code remains reusable. Start a fresh transaction for retry.
CODE VS TOKEN
Code on the front. Token on the back.
Follow a code through the browser, then watch the server check it before issuing an access token.
Kael has approved the grant. id.chronos.orbital has issued code C61; Chronos Mail keeps verifier V7.
More about this step
This confidential web client uses authorization code with S256 PKCE. The earlier authorization request, Kael’s authentication, and approval of mail.read are assumed complete. Chronos Mail C7 retained a fresh high-entropy verifier V7 and state S7 bound to Browser B7’s pending transaction. id.chronos.orbital AS has issued unused, unexpired C61 bound to C7, the registered callback CB, and challenge H7. The full expected issuer is configured before the flow; it is not taken from this response.
Chapter 1 of 8
Confidential web client with S256 PKCE, after authentication and approval. Decoded safe references, not live credentials or cryptography. Both channels use HTTPS. No API request, ID Token, refresh token, or browser session is shown.
Read all chapters
Nothing plays on its own. Watch all runs one overview. Replay this step stops on the selected chapter; Pause and Resume keep your place. Scrub or choose a chapter to inspect. Opening detail, leaving the picture, or hiding the tab pauses playback. Reduced motion shows still chapters with no travel.
Code + PKCE
- Kael has approved the grant. id.chronos.orbital has issued code C61; Chronos Mail keeps verifier V7.This confidential web client uses authorization code with S256 PKCE. The earlier authorization request, Kael’s authentication, and approval of mail.read are assumed complete. Chronos Mail C7 retained a fresh high-entropy verifier V7 and state S7 bound to Browser B7’s pending transaction. id.chronos.orbital AS has issued unused, unexpired C61 bound to C7, the registered callback CB, and challenge H7. The full expected issuer is configured before the flow; it is not taken from this response.
- id.chronos.orbital sends a redirect to the browser. It carries the code, state, and issuer over HTTPS.Over an authenticated HTTPS connection, id.chronos.orbital sends Browser B7 an HTTP 302 whose Location is CB with code=C61, state=S7, and iss equal to the configured id.chronos.orbital issuer. B7 records these values only after receipt. The browser-mediated return is the front channel. No access token is in this redirect. Cafe Wi-Fi cannot read the encrypted Location header or URL query.
- The browser follows the redirect to Chronos Mail. Receiving the code is not accepting it.Browser B7 follows Location with an HTTPS GET to CB, carrying C61, S7, and the issuer. Chronos Mail records the callback only after arrival. It has not yet accepted the response or contacted the token endpoint. The browser transports the code; it does not authenticate the client to the token endpoint.
- Chronos Mail checks the returned state and issuer against its pending browser transaction.Chronos Mail checks the required response parameters, the returned S7 against state bound to this pending browser transaction, and the decoded iss by exact string comparison with its previously configured issuer. This example assumes all these checks pass. Client acceptance does not validate the authorization code at the server or create an access token.
- Chronos Mail sends the code, verifier, client authentication, and original callback URI to id.chronos.orbital.Chronos Mail sends an HTTPS POST to the configured id.chronos.orbital token endpoint. It includes grant_type=authorization_code, code=C61, code_verifier=V7, and redirect_uri equal to CB, plus valid client authentication. This example uses HTTP Basic client authentication with credential reference A7. Copies travel under TLS; the client retains its verifier and credential. id.chronos.orbital records receipt only after the POST arrives, before evaluating it.
- id.chronos.orbital validates the client, code, callback URI, and PKCE proof before issuing AT61.id.chronos.orbital authenticates C7, checks that C61 is unused and unexpired and was issued to C7, requires redirect_uri to be identical to CB used in the authorization request, and verifies S256(V7)=H7. Only after these checks pass does id.chronos.orbital consume C61 and issue opaque bearer token AT61 for the approved mail.read grant. The client still has not received AT61. Check ordering and consumption timing are authored for this example.
- Only now does Chronos Mail receive the access token in the protected token-endpoint response.id.chronos.orbital returns an HTTPS HTTP 200 JSON response containing access_token=AT61 and token_type=Bearer, with no-store/no-cache response headers. Chronos Mail records AT61 only after arrival. AT61 is held by the backend and is never returned in the browser redirect. No resource API request, refresh token, ID Token, or application session creation is modeled.
- Chronos Mail holds a token for the Mail API. The API still has to validate it and permit the request.Chronos Mail now holds AT61; this does not mean the Mail API has accepted any request. The resource server must validate a later token-bearing request and apply its permissions. OAuth access tokens delegate access; they are not authentication assertions or proof that a particular human is currently at the keyboard. Each comparison starts a separate authored transaction, not a retry of a live code.
Wrong verifier
- Kael has approved the grant. id.chronos.orbital has issued code C61; Chronos Mail keeps verifier V7.This confidential web client uses authorization code with S256 PKCE. The earlier authorization request, Kael’s authentication, and approval of mail.read are assumed complete. Chronos Mail C7 retained a fresh high-entropy verifier V7 and state S7 bound to Browser B7’s pending transaction. id.chronos.orbital AS has issued unused, unexpired C61 bound to C7, the registered callback CB, and challenge H7. The full expected issuer is configured before the flow; it is not taken from this response.
- id.chronos.orbital sends a redirect to the browser. It carries the code, state, and issuer over HTTPS.Over an authenticated HTTPS connection, id.chronos.orbital sends Browser B7 an HTTP 302 whose Location is CB with code=C61, state=S7, and iss equal to the configured id.chronos.orbital issuer. B7 records these values only after receipt. The browser-mediated return is the front channel. No access token is in this redirect. Cafe Wi-Fi cannot read the encrypted Location header or URL query.
- The browser follows the redirect to Chronos Mail. Receiving the code is not accepting it.Browser B7 follows Location with an HTTPS GET to CB, carrying C61, S7, and the issuer. Chronos Mail records the callback only after arrival. It has not yet accepted the response or contacted the token endpoint. The browser transports the code; it does not authenticate the client to the token endpoint.
- Chronos Mail checks the returned state and issuer against its pending browser transaction.Chronos Mail checks the required response parameters, the returned S7 against state bound to this pending browser transaction, and the decoded iss by exact string comparison with its previously configured issuer. This example assumes all these checks pass. Client acceptance does not validate the authorization code at the server or create an access token.
- The comparison changes only the sent verifier to V8. Chronos Mail still retains V7.Chronos Mail sends an HTTPS POST to the configured id.chronos.orbital token endpoint. It includes grant_type=authorization_code, code=C61, code_verifier=V8, and redirect_uri equal to CB, plus valid client authentication. This example uses HTTP Basic client authentication with credential reference A7. V8 is a controlled different verifier whose S256 value does not match H7. The stored verifier remains V7. Copies travel under TLS; the client retains its verifier and credential. id.chronos.orbital records receipt only after the POST arrives, before evaluating it.
- Client authentication and code checks pass. PKCE does not match, so id.chronos.orbital refuses the grant.id.chronos.orbital authenticates C7 and finds C61 unused, unexpired, and issued to C7. It refuses this request because the received V8 does not produce the bound S256 challenge H7. The original URI check still passes. No token is issued. The response will be HTTP 400 with error=invalid_grant. This authored server leaves the code unused on these failed checks; that policy is not a general guarantee about code lifetime or failure handling.
- Chronos Mail receives HTTP 400 with invalid_grant. The error response contains no access token.id.chronos.orbital returns an HTTPS HTTP 400 JSON error=invalid_grant. Chronos Mail records that response only after arrival. It receives no access token. The browser’s earlier code delivery and Chronos Mail’s state and issuer checks still hold; the refusal is specifically at token-endpoint validation.
- No token was issued or received. The earlier browser callback and client checks still hold.This request ended with invalid_grant and no token. The earlier callback, matched state, expected issuer, valid client authentication, and code binding remain visible. Each path is a separate authored transaction with safe reference labels, not a retry against the same real code. OAuth tokens are not human authentication assertions; no resource API request is shown.
Different URI
- Kael has approved the grant. id.chronos.orbital has issued code C61; Chronos Mail keeps verifier V7.This confidential web client uses authorization code with S256 PKCE. The earlier authorization request, Kael’s authentication, and approval of mail.read are assumed complete. Chronos Mail C7 retained a fresh high-entropy verifier V7 and state S7 bound to Browser B7’s pending transaction. id.chronos.orbital AS has issued unused, unexpired C61 bound to C7, the registered callback CB, and challenge H7. The full expected issuer is configured before the flow; it is not taken from this response.
- id.chronos.orbital sends a redirect to the browser. It carries the code, state, and issuer over HTTPS.Over an authenticated HTTPS connection, id.chronos.orbital sends Browser B7 an HTTP 302 whose Location is CB with code=C61, state=S7, and iss equal to the configured id.chronos.orbital issuer. B7 records these values only after receipt. The browser-mediated return is the front channel. No access token is in this redirect. Cafe Wi-Fi cannot read the encrypted Location header or URL query.
- The browser follows the redirect to Chronos Mail. Receiving the code is not accepting it.Browser B7 follows Location with an HTTPS GET to CB, carrying C61, S7, and the issuer. Chronos Mail records the callback only after arrival. It has not yet accepted the response or contacted the token endpoint. The browser transports the code; it does not authenticate the client to the token endpoint.
- Chronos Mail checks the returned state and issuer against its pending browser transaction.Chronos Mail checks the required response parameters, the returned S7 against state bound to this pending browser transaction, and the decoded iss by exact string comparison with its previously configured issuer. This example assumes all these checks pass. Client acceptance does not validate the authorization code at the server or create an access token.
- The comparison changes only the redemption URI to /other. The browser still returned to /cb.Chronos Mail sends an HTTPS POST to the configured id.chronos.orbital token endpoint. It includes grant_type=authorization_code, code=C61, code_verifier=V7, and redirect_uri=https://mail.chronos.orbital/other, plus valid client authentication. This example uses HTTP Basic client authentication with credential reference A7. That URI is separately registered but differs from the original CB binding. The callback already arrived at /cb; this is not a redirect to another website. Copies travel under TLS; the client retains its verifier and credential. id.chronos.orbital records receipt only after the POST arrives, before evaluating it.
- Client authentication and code checks pass. The URI differs, so id.chronos.orbital refuses before PKCE.id.chronos.orbital authenticates C7 and finds C61 unused, unexpired, and issued to C7. It refuses this request because the redemption URI /other differs from the original CB (/cb). This server stops before evaluating PKCE; the diagram does not claim that check ran. No token is issued. The response will be HTTP 400 with error=invalid_grant. This authored server leaves the code unused on these failed checks; that policy is not a general guarantee about code lifetime or failure handling.
- Chronos Mail receives HTTP 400 with invalid_grant. The error response contains no access token.id.chronos.orbital returns an HTTPS HTTP 400 JSON error=invalid_grant. Chronos Mail records that response only after arrival. It receives no access token. The browser’s earlier code delivery and Chronos Mail’s state and issuer checks still hold; the refusal is specifically at token-endpoint validation.
- No token was issued or received. The earlier browser callback and client checks still hold.This request ended with invalid_grant and no token. The earlier callback, matched state, expected issuer, valid client authentication, and code binding remain visible. Each path is a separate authored transaction with safe reference labels, not a retry against the same real code. OAuth tokens are not human authentication assertions; no resource API request is shown.
Show as a table
A browser carries an authorization code to a confidential web client. The client checks the callback and sends a protected code + PKCE exchange. The server validates the grant, then issues a token; receipt follows response arrival. Wrong verifier or URI produces invalid_grant without a token.
| Chapter / assumption | What the model establishes |
|---|---|
| Context | Kael is the resource owner. Browser B7 carries redirects; Chronos Mail C7 is a confidential web client. id.chronos.orbital AS issues tokens. Mail API is the resource server, outside this exchange. |
| Safe references | C61, V7, V8, H7, S7, A7, RFA, and AT61 are synthetic labels, not protocol values or credentials. |
| Configured issuer | RFA means https://id.chronos.orbital. Chronos Mail knows this exact expected issuer and its endpoints before starting; a received iss does not choose a new token endpoint. |
| Original callback | CB means https://mail.chronos.orbital/cb. This exact registered URI was sent in the authorization request and bound to C61. Browser GET and successful redemption use it unchanged. |
| Pending transaction | Before these chapters, C7 saved a fresh high-entropy V7 and random S7 bound to B7’s pending transaction. It sent code_challenge=H7 and code_challenge_method=S256, client_id=C7, response_type=code, CB, and state=S7. |
| S256 proof | H7 denotes BASE64URL(SHA256(ASCII(V7))). The server binds H7 and the S256 method to C61. A verifier is sent only in the HTTPS token POST, never in the browser callback. |
| Redirect | id.chronos.orbital sends B7 an HTTPS HTTP 302 Location with CB, code=C61, state=S7, and iss=RFA. The browser records the code after arrival, then follows the Location. |
| Callback | B7 sends an HTTPS GET to CB carrying those response parameters. C7 records receipt, then checks required fields, the transaction-bound state, and the exact expected issuer separately. |
| Front and back | Front channel means browser-mediated delivery. Back channel means the client backend calls the token endpoint directly. Both use authenticated HTTPS; front does not mean public plaintext. |
| Passive Wi-Fi | Cafe Wi-Fi has no endpoint access and cannot read the encrypted redirects, queries, POSTs, or token responses. It may observe traffic metadata; endpoint compromise and other URL leakage are outside this example. |
| Redeem | C7 posts grant_type=authorization_code, C61, V7, and the identical CB to the configured token endpoint. Valid HTTP Basic uses credential A7. Protected copies leave the stored verifier and credential at the client. |
| Server checks | The server authenticates C7, checks the unused/unexpired code and client binding, exact original URI, and S256 verifier. It consumes C61 and issues AT61 only after success, before replying to C7. |
| Wrong verifier | Only the sent verifier changes to V8. Client authentication, code, and original URI checks still pass; S256(V8) does not equal H7. HTTP 400 invalid_grant carries no token. |
| Different URI | Only the token POST redirect_uri changes to https://mail.chronos.orbital/other. Separate registration does not satisfy equality with CB. Browser delivery was to /cb; this server refuses before PKCE. |
| Failure policy | These two refusals leave C61 unused in this authored server. Check order and failed-code lifetime are not universal guarantees. Comparisons are fresh independent transactions, not real code reuse. |
| Token reply | Success is HTTP 200 JSON access_token=AT61, token_type=Bearer with no-store/no-cache headers. The backend records AT61 after response arrival. No access token appears in the browser redirect. |
| Later API use | AT61 is an opaque bearer token for an approved mail.read grant. A later Mail API request still requires resource-server validation and authorization. Neither API acceptance nor a refresh token is shown. |
| Identity limit | An access token delegates access, not a current human at the keyboard. An OIDC ID Token needs validation of authentication claims, issuer, audience, time, transaction binding, and applicable cryptographic checks. |
Scene checked 2026-09-30.
Check the picture
In the OAuth 2.0 Authorization Code flow with PKCE, why are callback checks and back-channel redemption kept strictly separate?
Why confidential web clients still use PKCE
PKCE (RFC 7636) was originally invented for mobile apps that cannot hold a static client secret, but RFC 9700 recommends PKCE for all OAuth clients, including confidential server-side web apps. Why? Because even if an attacker intercepts an authorization code (C61) via a compromised browser extension or log leak and attempts to inject C61 into their own login session on the same confidential client (an authorization code injection attack), the attacker’s session holds a different code_ in server memory, causing the PKCE check at the Authorization Server to fail with invalid_.
CHECK YOUR JUDGMENT