Ndovu elephant mark ndovu

PKCE consolekeys readyruns in your browser · zero network calls - nothing leaves this page

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
nothing yet - start the flow and every request lands here

RFC 7636 · AUTHORIZATION CODE + PKCE

The PKCE console

0:00 / 5:34Read along

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.

updated October 29, 2026The flow, on the wire

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.

What to take away
  • The challenge is public; the verifier never leaves the client until the exchange. Anything less is not PKCE.
  • state is 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.
  • plain is deprecated for a reason: with S256, intercepting the challenge reveals nothing.
The attack

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.

The flow

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).

PKCE abstract protocol flow: client, authorization server, and where each key travelsClient (public)Authorization serverkeeps: code_verifierstores: code_challengeA. authorize request + code_challenge (S256), stateB. authorization code (bound to the challenge)the code rides through the browser - the verifier does notC. token request: code + code_verifier (back channel, TLS)server: BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) == code_challenge ?D. match: access tokenno match: 400 invalid_grantan attacker who intercepted the code at step B fails here -they hold a ticket with no barcode
The mechanics

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.

The defenses

What each piece is for

PieceStopsWhere 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_verifierCode 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
stateForged 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 codesReplay: a redeemed code fails the next exchange (RFC 6749 section 5.2, invalid_grant)server-side code store
exact redirect_uri matchRedirecting 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.

References