In PKCE, which OAuth 2.0 request carries the `code_challenge` and which carries the `code_verifier`?
answer
- two legs, two different values
- the browser only ever sees the digest
- the server stores the challenge with the code
- raw verifier on the token request
- recompute and compare, else invalid_grant
basics
~20 sThe code_challenge and code_challenge_method travel on the front-channel authorization request; the raw code_verifier travels on the back-channel token request, where the authorization server re-derives the challenge and compares it with the one stored against that code.
solid answer
~40 s`code_challenge` and `code_challenge_method` are authorization-request parameters, so they go out through the browser. The authorization server stores them against the code it issues — it never sees the verifier at that point. `code_verifier` is a token-request parameter, POSTed directly to the `token_endpoint` in the form-encoded body, so it never touches the browser. At redemption the server retrieves the stored challenge and method, recomputes `BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))` for `S256`, and compares. A mismatch, or a missing verifier where a challenge was stored, is answered with `400` and `error` set to `invalid_grant`. The asymmetry is the design: only the transformed value is exposed where things can watch.
code
http · 6 linesGET /authorize?response_type=code&client_id=s6BhdRkqt3
&state=af0ifjsldkj&scope=jobs.submit
&redirect_uri=https%3A%2F%2Fplugin.example%2Fcb
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256 HTTP/1.1
Host: as.examplego deeper
Remember the direction: the challenge goes out through the browser, the verifier goes out on the direct request to the token endpoint, and only the server compares them.
Walk the whole exchange: what the server persists with the code, what it recomputes at redemption, and the exact error and status a mismatch produces.
Bring the failure modes. Most real PKCE incidents are a verifier that did not survive between the two legs, and the symptom is an invalid_grant that looks like an expired code.
Consider what the client must keep between the legs across restarts, multiple concurrent flows and multiple devices, and what that storage costs you in complexity against the guarantee it buys.
## Two values, two channels PKCE adds three parameter names to the authorization-code grant, and the whole mechanism is a matter of which one travels where. `code_challenge` and `code_challenge_method` are **authorization-request** parameters: they go out on the front channel, in the query string the user's browser carries to the `authorization_endpoint`. `code_verifier` is a **token-request** parameter: it goes out on the back channel, in the `application/x-www-form-urlencoded` body the client POSTs to the `token_endpoint`. That asymmetry is the design. Everything on the front channel is visible to the browser, to other software on the same machine, and to whatever the redirect passes through; only the transformed value is ever put there. The value that actually proves possession never leaves the client until it is sent, TLS-protected, straight to the token endpoint. | Value | Request it belongs to | Channel | Who can see it | |---|---|---|---| | `code_challenge` | authorization request | front | the browser and anything watching it | | `code_challenge_method` | authorization request | front | the same | | `code_verifier` | token request | back | the client and the authorization server | | `code` | authorization response | front | the browser and anything watching it | ## What the authorization server stores When the authorization server issues the code, it records the `code_challenge` and the `code_challenge_method` **against that code**. It does not store the verifier, because it has never seen one. This is the part candidates most often get backwards: the server holds the commitment and waits, while the client holds the secret and produces it exactly once. ## The check at the token endpoint 1. The token request arrives with `grant_type=authorization_code`, the `code`, the `redirect_uri` from the authorization request, the `client_id`, and `code_verifier`. 2. The server looks up the code and retrieves the `code_challenge` and `code_challenge_method` stored with it. 3. If the method is `S256` it computes `BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))` over the verifier exactly as received; if the method is `plain` it takes the verifier as it stands. 4. It compares the computed value against the stored challenge. 5. Equal, and the exchange proceeds to a token response. Not equal, or no verifier at all where a challenge was stored, and the request is refused. ## The error, and what it tells you A failed PKCE check is a problem with the grant being presented, not with the identity of the caller, so the token endpoint answers `400 Bad Request` with `error` set to **`invalid_grant`** — the same error as an expired or already-redeemed code. It is deliberately unspecific: the response must not reveal whether the code was unknown, expired, already used, or bound to a different challenge. Two consequences follow for anyone debugging a flow: - `invalid_grant` on a flow that used to work is as likely to be a verifier regenerated between the two legs as an expired code. A client that creates a fresh verifier when it builds the **token** request, instead of remembering the one behind the challenge it already sent, fails this way deterministically, every single time. - `invalid_client` means something else entirely — the client's own credential at the token endpoint was rejected. Reading one as the other sends the investigation to the wrong side of the exchange. ## Where implementations get this wrong - Putting the `code_verifier` on the authorization request, which hands the secret straight to the front channel and throws away everything the transformation bought. - Sending a `code_challenge` again on the token request, as though the server needed reminding; the server checks the value it stored, and the one on the request is not consulted. - Keeping the pending verifier in a slot that is not per-flow, so a second authorization request started in the same client overwrites it and the first flow dies at redemption. - Transforming a re-encoded form of the verifier: `S256` is computed over the ASCII octets of the verifier string exactly as it will be sent, so a stray padding character or a re-encode changes the digest and the comparison fails. The shape worth keeping in your head is a single question asked across two channels: the client states its commitment where everything can see it, and redeems that commitment where only the authorization server can hear.
- Why does the authorization server store the challenge rather than asking the client to resend it at redemption?Because a value the redeemer supplies proves nothing about the request that produced the code. The commitment has to be captured at authorization time, when the legitimate client was the one talking, and then held by the server. A challenge accepted on the token request would let whoever holds the code choose both halves of the pair.
- A client sees invalid_grant only on its very first login after a restart. Where would you look?At where the pending `code_verifier` lives. If it is held in memory that the restart cleared, or in a slot overwritten by a second flow, the client sends a verifier that does not belong to the challenge stored against that code. Confirm by logging the challenge the client computed at authorization time and comparing it with the one it would compute at redemption.
saying these in an interview costs you the question
- Sends the code_verifier on the authorization request
- Thinks the authorization server stores the verifier, not the challenge
- Expects a fresh code_challenge on the token request as well
- Assumes the client compares the two values rather than the server
- Says a wrong code_verifier is answered with invalid_client