skip to content

In an OAuth 2.0 authorization-code flow, what attack does PKCE (RFC 7636) exist to stop?

level: juniorimportance: must knowfreq 66%

answer

  1. the code rides the front channel
  2. a stolen code needs a second half
  3. one fresh secret per authorization request
  4. challenge out front, verifier out back
  5. no matching verifier, invalid_grant

basics

~20 s

PKCE stops authorization-code interception. An attacker who captures the code as it travels back through the browser cannot redeem it, because the token request must also carry a code_verifier that only the application which started the flow holds.

solid answer

~40 s

The authorization code comes back through the front channel — a redirect the user's browser performs — so anything that can see or receive that redirect can read the code. RFC 7636 calls that authorization-code interception: whatever holds the code POSTs it to the `token_endpoint` and walks away with an `access_token`. PKCE makes the code insufficient on its own. Before the flow starts the client invents a fresh random `code_verifier`, puts only its transformed form on the authorization request as `code_challenge`, and sends the raw `code_verifier` on the back-channel token request. The authorization server re-derives the challenge, compares it with the one stored against that code, and answers `invalid_grant` when they differ. It binds the code to whoever started the flow; it does not authenticate the client.

code

http · 3 lines
http
HTTP/1.1 302 Found
Location: https://plugin.example/cb?code=SplxlOBeZQQYbYS6WxSbIA
    &state=af0ifjsldkj

go deeper

for a junior

Be able to say in one line what PKCE protects against: a code captured on its way back through the browser cannot be exchanged without the verifier the original application kept.

for a middle

Explain both legs and the comparison the authorization server performs, name the transformation behind S256, and name the error returned when the verifier does not match the stored challenge.

for a senior

Show where the protection ends. PKCE binds a code to its request; it says nothing about who the client is, whether the issued token leaks later, or whether the response came from the issuer you asked.

for a principal

Argue it as a default rather than a platform feature: enforcing it across every client costs one parameter pair per flow, while one intercepted code costs an account. The interesting trade-off is enforcement policy, not mechanism.

## Where an authorization code is exposed The authorization-code grant splits one exchange over two channels. The **front channel** is the user's browser: the client sends the user to the authorization server's `authorization_endpoint`, the user authenticates and approves, and the server sends the browser back to the client's redirection endpoint with `code` and `state` on it. The **back channel** is a direct, TLS-protected POST to the `token_endpoint` that no browser is involved in. That split exists because the `access_token` is the prize and it is only ever delivered on the back channel. But the `code` is still a credential: whatever presents it at the token endpoint, with the same `client_id` and `redirect_uri`, receives a token. Before PKCE the only things between an intercepted code and a token were single use, a short lifetime, and — for a client able to keep one — a `client_secret`. An application distributed to every member's own machine has no secret to keep: every copy ships the same bytes to everybody. ## Authorization-code interception, step by step RFC 7636 — *Proof Key for Code Exchange* — is written against one concrete attack: 1. A legitimate application starts an authorization-code flow and hands the user to the authorization server. 2. The user authenticates and approves, and the server issues a code and directs the browser back toward the application. 3. Something else receives or observes that response and reads the code out of it. 4. The interceptor POSTs the code to the token endpoint, before or instead of the real application. 5. The authorization server cannot tell the two apart: the code is live, the `redirect_uri` matches, and there is no secret to check. Step 5 is the hole. Single use does not close it, because whoever redeems first wins and the interceptor is usually faster than the user's own application. A short lifetime narrows the window rather than closing it. Neither check ever asks the only question that matters: *is the party redeeming this code the party that asked for it?* ## What PKCE adds PKCE answers exactly that question, with one secret invented per authorization request: - the client generates a fresh, high-entropy **`code_verifier`**; - it derives a **`code_challenge`** from it — with `code_challenge_method=S256`, that derivation is `BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))`; - the challenge and the method ride the front-channel authorization request, and the authorization server stores them against the code it issues; - the raw `code_verifier` rides the back-channel token request; - the server re-derives the challenge from the verifier it received and compares it with the stored one; no match, or no verifier where a challenge was stored, and it answers `invalid_grant`. | | Without PKCE | With PKCE using `S256` | |---|---|---| | What the front channel carries | the `code` on the way back | the `code_challenge` out, the `code` back | | What an interceptor of the code holds | everything needed to redeem it | one half of a pair | | What the token endpoint checks | the code is live and unused | that, plus the verifier's digest | | Result of presenting a stolen code | an `access_token` | `invalid_grant` | ## What PKCE does not do Being precise about the limit is most of what separates a good answer here. - **It does not authenticate the client.** Anybody can generate a verifier. PKCE binds a code to the request that produced it and says nothing about who that requester was; a client that must prove its own identity does so separately at the token endpoint. - **It does not encrypt anything.** The code is exactly as readable in the redirect as it ever was — it is simply no longer sufficient. - **It is not CSRF protection.** Binding the authorization response to the user's own session is what the `state` parameter is for, and PKCE does not replace it. - **It ends at the token endpoint.** What happens to the `access_token` after issuance is a separate problem with separate mechanisms. ## Not a mobile-only feature PKCE arrived for applications installed on users' devices, because that is where interception is easiest and where there is no secret to fall back on. The OAuth 2.0 Security Best Current Practice, RFC 9700 §2.1.1, has since generalised it: public clients **MUST** use PKCE, it is **RECOMMENDED** for confidential clients, and authorization servers **MUST** support it and **MUST** enforce the `code_verifier` whenever a `code_challenge` was present on the authorization request. Treating PKCE as a platform-specific add-on rather than the default shape of the code grant is the answer an interviewer is listening for.

  • A code is single-use and expires in under a minute — why is that not already enough against interception?
    Single use only means the first redeemer wins, and an interceptor is usually faster than the user's own application. A short lifetime shrinks the window instead of closing it. Neither check asks whether the party redeeming the code is the party that requested it, and that is the only question `code_verifier` answers.
  • Does PKCE help a client that already authenticates itself at the token endpoint?
    Yes, which is why RFC 9700 §2.1.1 makes it a MUST for public clients and RECOMMENDED for confidential ones. Client authentication proves who is redeeming; PKCE proves the redemption belongs to one specific authorization request. They answer different questions, and an authorization server that enforces only the first still accepts a code that was never bound to a request.
  • Does PKCE protect the access token once it has been issued?
    No. Its job ends at the token endpoint: it binds one authorization code to one authorization request. Where the token is then kept, how it is carried on API calls and how it is revoked are separate problems addressed by separate mechanisms.

You snap a padlock shut at the counter when you drop the job off and keep the only key in your pocket. The collection slip stays perfectly readable, and a thief who lifts it still meets a lock they cannot open.

saying these in an interview costs you the question

  • Says PKCE authenticates the client to the authorization server
  • Claims PKCE encrypts the authorization code in transit
  • Treats PKCE as a mobile-only add-on rather than the default
  • Thinks PKCE removes the need for the state parameter
  • Says the code_verifier travels on the authorization request