✳ Field lesson IF1a / 8 min

OAuth 2.0 Authorization Code + PKCE: front channel vs back channel

The OAuth 2.0 Authorization Code flow with PKCE (RFC 6749, RFC 7636, RFC 9207, RFC 9700) separates the browser redirect (front channel) from token issuance (back channel). The browser only carries a one-time authorization code (C61), which the confidential client redeems over a direct authenticated HTTPS POST by proving its client secret, exact redirect URI, and PKCE code_verifier (V7).

identityoauthauthorization-codessofoundations-electiveCISSP D5Security+ (SY0-701) D4CCSP D4

A SMALL WORLD / TWO CHANNELS

ONE CODE.

A browser carries a code. A server trades it for a token.
Follow the handoff. Open the message. Change one check.

The callback matches its pending transaction. The token endpoint accepts the grant, then returns a token to the web client.

Local work in progress

A code is ready

Authorization and user consent are complete. The web client retains a transaction-bound verifier V7 and state S7. The authorization server has issued unused C61, bound to this client, /cb, and S256 challenge H7.

CHANGE YOUR SEAT

Web client evidence

The world stays in place. This panel shows what the selected actor knows at this instant.

Local verifier
V7 · stays in client state
Callback code
Not received yet
Token response
Not received yet
Try this

Pause the token response in transit. Compare the server’s evidence with the client’s. Then switch to the browser.

Who already knows the token? Who has received it? Who will never receive this response?

Teaching model. One confidential web client using Authorization Code + S256 PKCE over HTTPS. User authentication, consent, and initial authorization-request validation are already complete. C61, V7, H7, and AT61 are symbolic values, not real credentials or cryptography. Timings are illustrative. The API validation is summarized; refresh tokens, public-client flows, and subsequent browser rendering are outside this model.

Read every journey and outcome

Matching transaction

The callback matches its pending transaction. The token endpoint accepts the grant, then returns a token to the web client.

  1. A code is ready. Authorization and user consent are complete. The web client retains a transaction-bound verifier V7 and state S7. The authorization server has issued unused C61, bound to this client, /cb, and S256 challenge H7.
  2. The browser receives a code. An HTTPS redirect carries code, state, and issuer through the browser. It carries no access token. The browser learns the callback contents only on arrival.
  3. The browser carries it back. The browser requests the registered /cb URL over HTTPS. Receiving C61 is not acceptance of the callback and does not create an access token.
  4. The client checks its transaction. The client compares state with its pending browser transaction and issuer with its configured expected issuer. Both match. These client checks do not yet validate the code at the authorization server.
  5. A separate back-channel request. The confidential web client sends an authenticated HTTPS POST directly to its configured token endpoint. Submitted verifier V7 travels on this connection, not through the browser callback. The client retains V7 locally.
  6. The server issues a token. The server verifies client authentication, code ownership and freshness, exact redirect URI, and the S256 verifier binding. It consumes C61 and issues AT61 for mail.read. The client has not received AT61 yet.
  7. The token reaches the client. The server already knows AT61. Its HTTPS response returns the token directly to the web client. The client can use it only after receipt; the browser does not receive this response.
  8. Use the token at its resource. After receiving AT61, the client presents it over HTTPS to the Mail API. This is a resource request, not a second authorization-code redemption.
  9. The API makes its own decision. The API validates the token for this resource and checks mail.read and the requested user’s mailbox. This example assumes a valid, unexpired token and an allowed request. Token possession alone is not a universal permission.
  10. The requested mail returns. The API sends the permitted synthetic result. The client records it at arrival. The browser has carried a code; the confidential client has held and used the access token.

Wrong verifier

Change only the submitted verifier to V0. Client authentication still passes, but the code is bound to H7, so the token endpoint returns invalid_grant.

  1. A code is ready. Authorization and user consent are complete. The web client retains a transaction-bound verifier V7 and state S7. The authorization server has issued unused C61, bound to this client, /cb, and S256 challenge H7.
  2. The browser receives a code. An HTTPS redirect carries code, state, and issuer through the browser. It carries no access token. The browser learns the callback contents only on arrival.
  3. The browser carries it back. The browser requests the registered /cb URL over HTTPS. Receiving C61 is not acceptance of the callback and does not create an access token.
  4. The client checks its transaction. The client compares state with its pending browser transaction and issuer with its configured expected issuer. Both match. These client checks do not yet validate the code at the authorization server.
  5. A separate back-channel request. The confidential web client sends an authenticated HTTPS POST directly to its configured token endpoint. It submits changed verifier V0 on this connection, not through the browser callback. The client retains its original verifier V7 locally.
  6. The server refuses the grant. Client authentication, code validity, and redirect URI checks pass. S256(V0) differs from stored H7. The server issues no token and prepares invalid_grant.
  7. A refusal travels back. The server already knows the refusal it is sending. The client records HTTP 400 invalid_grant only after receipt. No access token exists in this outcome.

Wrong callback state

Return S0 instead of S7. The web client refuses this callback before sending a token request.

  1. A code is ready. Authorization and user consent are complete. The web client retains a transaction-bound verifier V7 and state S7. The authorization server has issued unused C61, bound to this client, /cb, and S256 challenge H7.
  2. The browser receives a code. An HTTPS redirect carries code, state, and issuer through the browser. It carries no access token. The browser learns the callback contents only on arrival.
  3. The browser carries it back. The browser requests the registered /cb URL over HTTPS. Receiving C61 is not acceptance of the callback and does not create an access token.
  4. The client refuses the callback. Returned state S0 does not match the pending transaction S7. The client stops here. No token request is sent; grant and PKCE checks have not run.

Unexpected issuer

Return an unexpected issuer identifier. The web client refuses it before selecting or calling a token endpoint.

  1. A code is ready. Authorization and user consent are complete. The web client retains a transaction-bound verifier V7 and state S7. The authorization server has issued unused C61, bound to this client, /cb, and S256 challenge H7.
  2. The browser receives a code. An HTTPS redirect carries code, state, and issuer through the browser. It carries no access token. The browser learns the callback contents only on arrival.
  3. The browser carries it back. The browser requests the registered /cb URL over HTTPS. Receiving C61 is not acceptance of the callback and does not create an access token.
  4. The client refuses the callback. The returned issuer differs from the client’s previously configured expected issuer. The client stops here. It does not follow the unexpected issuer to a token endpoint.

Different redemption URI

Send /other instead of the original /cb during redemption. The authorization server refuses the binding mismatch before evaluating PKCE in this model.

  1. A code is ready. Authorization and user consent are complete. The web client retains a transaction-bound verifier V7 and state S7. The authorization server has issued unused C61, bound to this client, /cb, and S256 challenge H7.
  2. The browser receives a code. An HTTPS redirect carries code, state, and issuer through the browser. It carries no access token. The browser learns the callback contents only on arrival.
  3. The browser carries it back. The browser requests the registered /cb URL over HTTPS. Receiving C61 is not acceptance of the callback and does not create an access token.
  4. The client checks its transaction. The client compares state with its pending browser transaction and issuer with its configured expected issuer. Both match. These client checks do not yet validate the code at the authorization server.
  5. A separate back-channel request. The confidential web client sends an authenticated HTTPS POST directly to its configured token endpoint. Submitted verifier V7 travels on this connection, not through the browser callback. The client retains V7 locally.
  6. The server refuses the grant. Client authentication and code checks pass. The redemption URI differs from the original /cb binding. This server stops before PKCE, issues no token, and prepares invalid_grant.
  7. A refusal travels back. The server already knows the refusal it is sending. The client records HTTP 400 invalid_grant only after receipt. No access token exists in this outcome.

Failed-redemption code lifetime is deliberately not modeled. It is not safe to assume that a code remains reusable after a failed attempt. Repair starts a fresh illustrative transaction.

Sources: RFC 6749 §4.1, RFC 7636 §4.6, RFC 9207 §2.4, and RFC 9700 §4.5.

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_uri, and PKCE 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.

Defense-in-depth checks across the OAuth 2.0 Authorization Code + PKCE flow
Protocol ParameterChecked ByWhen CheckedAttack Prevented
state (high-entropy random)Confidential Web ClientStep 3: upon receiving browser callback before calling /tokenCross-Site Request Forgery (CSRF) login/account-linking fixation on the callback endpoint
iss (RFC 9207 issuer)Confidential Web ClientStep 3: alongside state on the callback query stringAuthorization Server Mix-Up attacks in multi-IdP environments
redirect_uri exact matchAuthorization ServerStep 1 (authorize) & Step 5 (token redemption)Redirecting codes to attacker-controlled URIs or open redirectors
PKCE (code_challenge / code_verifier V7)Authorization ServerStep 5: hashes incoming V7 and compares to stored challenge for C61Authorization 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_verifier (V7), keeping V7 in local memory while sending only its hash (code_challenge) 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_uri, and the secret code_verifier (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_uri mismatches the callback URI (URI: different, so PKCE: not run), the server returns HTTP 400 invalid_grant (error=invalid_grant) 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.

Choose a path
Code then tokenBrowser B7Kael’s browserCode: waitingState: waitingIssuer: waitingToken: noneChronos MailWeb client C7Verifier: V7Auth key: localCode: waitingState: pendingIssuer: pendingURI: not sentToken: noneReply: waitingid.chronos.orbitalAuth server ASS256 → H7POST: waitingC61: unusedAuth: pendingCode: pendingURI: pendingPKCE: pendingIssued: noneResult: waitingHTTPSBoth legsApprovedAt serverNo tokenCafe Wi-FiPassive network observerTLS payload unreadableAPI permission comes later

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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.

Code on the front. Token on the back. in text
Chapter / assumptionWhat the model establishes
ContextKael 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 referencesC61, V7, V8, H7, S7, A7, RFA, and AT61 are synthetic labels, not protocol values or credentials.
Configured issuerRFA 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 callbackCB 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 transactionBefore 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 proofH7 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.
Redirectid.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.
CallbackB7 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 backFront 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-FiCafe 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.
RedeemC7 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 checksThe 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 verifierOnly 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 URIOnly 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 policyThese 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 replySuccess 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 useAT61 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 limitAn 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.

Use this picture in the check below

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_verifier in server memory, causing the PKCE check at the Authorization Server to fail with invalid_grant.

CHECK YOUR JUDGMENT

An attacker intercepts a legitimate user’s one-time authorization code (C61) from a compromised local browser debugger during a login to Meridian’s portal (portal.chronos.orbital) and replays C61 into the callback URL of the attacker’s own browser session on portal.chronos.orbital. Even though portal.chronos.orbital authenticates to the token endpoint with its valid confidential client secret, the Authorization Server rejects the token exchange with HTTP 400 invalid_grant. Which control stopped the code injection?

Put your learning to work ↗

Find your next idea.

Tip: press / to open search. Escape closes this window.