PKCE
Proof Key for Code Exchange (RFC 7636) — extension to the Authorization Code Flow that prevents authorization code interception. Mandatory for public clients in OAuth 2.1; recommended for all clients.
What It Defends Against
The “authorization code interception attack”: a malicious app on the same device (or a malicious network intermediary, in less-protected environments) sees the authorization code as it flies past in the redirect, and tries to exchange it for tokens. Without PKCE — and with no client secret (public client) — the attacker succeeds.
The Mechanism
- Client generates a high-entropy random
code_verifier(43–128 chars, URL-safe). - Client computes
code_challenge = BASE64URL(SHA256(code_verifier)). - Client sends
code_challenge+code_challenge_method=S256in the authorization request. - Auth server stores the challenge bound to the issued code.
- When client exchanges the code, it sends the original
code_verifier. - Server verifies
BASE64URL(SHA256(code_verifier)) == stored code_challenge. Mismatch → rejection.
sequenceDiagram participant C as Client participant AS as Auth Server participant A as Attacker Note over C: Generate verifier (secret)<br/>challenge = SHA256(verifier) C->>AS: /authorize ?code_challenge=CHALLENGE AS-->>C: 302 ?code=CODE A->>A: Intercepts CODE A->>AS: /token code=CODE (no verifier) AS-->>A: 400 — verifier required/invalid C->>AS: /token code=CODE & code_verifier=VERIFIER AS-->>C: tokens
Why SHA256 and Not the Verifier Itself?
The challenge is sent in the (possibly logged, possibly intercepted) authorization request. If the challenge were the verifier, an attacker who saw the authorization request would have the verifier. Hashing means the verifier never leaves the client until token exchange — which is server-to-server (or, for public clients, the same trusted client process).
code_challenge_method=plain exists in the spec for legacy compatibility. Don’t use it. S256 only.
Public vs Confidential Clients
- Confidential client — runs on a server, can keep a
client_secret. Historically didn’t need PKCE (the secret played a similar role) but should use both per OAuth 2.1. - Public client — SPA, mobile app, CLI. Can’t keep a secret. PKCE replaces the secret as the “you’re really the original requester” proof.
Verifier Generation (Reference)
function base64url(buf) {
return btoa(String.fromCharCode(...new Uint8Array(buf)))
.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
}
const verifier = base64url(crypto.getRandomValues(new Uint8Array(32)));
const challenge = base64url(
await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))
);Pitfalls
- Storing the verifier in
localStorage. XSS reads it. PrefersessionStoragescoped to the auth tab, or in-memory state passed via the OS keychain on native. - Using
plainmethod. Defeats the purpose. - Re-using verifiers across requests. Each authorization request should generate a fresh verifier.
See Also
- OAuth 2.0
- Authorization Code Flow
- Sign in with Apple — historically lacked PKCE support; verify current status before relying on it