01 · the client and its two keys
- client_id
- lab-oauth2-playgroundpublic - it identifies, it does not authenticate
- redirect_uri
- https://etelej.com/lab/oauth2/playgrounds/pkce/cbthe AS will only redirect here
- code_verifier
- ...32 random bytes, base64url - stays on the client
- code_challenge
- ...base64url(SHA-256(verifier)), sent on the authorize request
verifier configuration (folded by default)
- code_challenge_method
- S256pinned - "plain" sends the verifier as its own challenge; deprecated (RFC 7636 section 4.4.1), and the downgrade scenario shows why
- verifier length
wire - everything that movednothing yet
RFC 7636 · AUTHORIZATION CODE + PKCE
The PKCE console
Run the full authorization-code + PKCE flow in your browser: generate the keys, redirect, sign in at the authorization server, verify state, redeem the code, inspect the token - and tamper with each defense to see it hold.
This is the whole authorization-code + PKCE flow, and you are the client. Every step is the real mechanism - the keys are genuine WebCrypto output, the checks are the checks a real client and server run. The wire drawer under the console records every request as it moves, and#scenario=downgrade restores a flow that was talked into plain - the deprecated challenge method - to show why S256 is the floor. Two buttons are deliberately dangerous: press them to watch each defense hold.
- The challenge is public; the verifier never leaves the client until the exchange. Anything less is not PKCE.
stateis the client's CSRF defense - it binds the callback to the request this client started.- Authorization codes are single-use and short-lived. PKCE is what makes a stolen code worthless, not the code's expiry.
plainis deprecated for a reason: with S256, intercepting the challenge reveals nothing.
The code interception problem
PKCE exists because of one specific attack, described in RFC 7636 section 1 and observed in the wild. On phones, apps receive OAuth redirects through custom URI schemes - and operating systems let any app register the same scheme as a legitimate app. A malicious app registers itself, waits for the redirect carrying the authorization code, and intercepts it.
With the code alone, the attacker walks to the token endpoint and redeems it - because in plain OAuth 2.0, possession of the code is the proof. The pre-conditions are narrow but real: the attacker needs the public client_id (every install of a native app shares it), the ability to observe the redirect, and the code grant in use.
PKCE's move: make the code worthless without a second, secret value that never crosses the browser. The attacker can hold the code forever and still fail the exchange.
Four hops, two keys
The abstract protocol flow from RFC 7636 section 1.1. The challenge travels on the front channel (through the browser); the verifier travels only on the back channel (client to token endpoint, over TLS).
Both keys, precisely
The code verifier. RFC 7636 section 4.1: a cryptographically random string of 43 to 128 characters from the unreserved set (A-Z a-z 0-9 - . _ ~). The recommended construction is 32 random octets, base64url-encoded - exactly 43 characters carrying at least 256 bits of entropy. A fresh verifier is minted for every authorization request; reusing one quietly rebuilds the shared-secret problem PKCE removed.
The code challenge. Section 4.2 defines two transforms.plain is the verifier unchanged - it exists only for legacy deployments and SHOULD NOT be used, because an attacker who reads the authorize request then holds the verifier. S256 isBASE64URL-ENCODE(SHA256(ASCII(code_verifier))), and it is mandatory to implement on servers. Clients MUST NOT downgrade to plain after trying S256 - per section 7.2, an S256 failure can only mean a broken server or a man-in-the-middle downgrade attempt.
The server's side. When the code is issued, the server binds the challenge and method to it (section 4.4) - typically storing them server-side or encrypted inside the code. At the token endpoint (section 4.6) it transforms the presented verifier and compares. Equal: proceed as normal. Not equal: invalid_grant, per RFC 6749 section 5.2. If PKCE is required and the challenge is missing entirely, the authorization endpoint answers invalid_request before any login happens (section 4.4.1).
Why no salt? Section 7.3: S256 resembles password hashing, but passwords are low-entropy and dictatable; the verifier carries 256 bits of true randomness, so there is nothing to brute-force and a public salt adds nothing.
What each piece is for
| Piece | Stops | Where it lives |
|---|---|---|
code_challenge (S256) | Eavesdroppers reading the authorize request - the challenge reveals nothing on its own (RFC 7636 section 7.2) | authorize request, public |
code_verifier | Code interception: the intercepted code cannot be redeemed without it (RFC 7636 sections 4.6, 4.5.3 of RFC 9700) | client only, sent once to the token endpoint |
state | Forged callbacks and code injection from other sessions - the CSRF check (RFC 6749 section 4.1.1; RFC 9700 section 4.7.1) | authorize request + callback |
| single-use codes | Replay: a redeemed code fails the next exchange (RFC 6749 section 5.2, invalid_grant) | server-side code store |
exact redirect_uri match | Redirecting codes to attacker-controlled endpoints (RFC 6749 section 3.1.2.3; RFC 9700 section 2.1) | both requests |
And the modern floor: since January 2025, RFC 9700 (the OAuth security best current practice) requires all public clients to use PKCE, recommends it for confidential clients too, and requires every authorization server to support it. S256 is the only registered method that does not expose the verifier in the authorize request - which is why the console pins S256 and shows plain only as the downgrade attack scenario.
- RFC 7636: Proof Key for Code Exchange by OAuth Public ClientsSakimura, Bradley, Agarwal (IETF, September 2015) - the normative text; sections 4.1-4.6 define everything the console demonstrates, section 7 the security analysis
- RFC 6749: The OAuth 2.0 Authorization Frameworkthe base grant the extension rides on
- RFC 9700: Best Current Practice for OAuth 2.0 SecurityLodderstedt et al. (IETF, January 2025) - public clients MUST use PKCE; S256 only; implicit grant deprecated
- RFC 8252: OAuth 2.0 for Native Appsthe custom-URI-scheme world the interception attack lives in
- oauth.net: PKCEAaron Parecki's annotated overview on the OAuth official site
- OWASP OAuth 2.0 Cheat Sheetindependent security checklist; its PKCE guidance matches the console's behavior