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

  1. Client generates a high-entropy random code_verifier (43–128 chars, URL-safe).
  2. Client computes code_challenge = BASE64URL(SHA256(code_verifier)).
  3. Client sends code_challenge + code_challenge_method=S256 in the authorization request.
  4. Auth server stores the challenge bound to the issued code.
  5. When client exchanges the code, it sends the original code_verifier.
  6. 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. Prefer sessionStorage scoped to the auth tab, or in-memory state passed via the OS keychain on native.
  • Using plain method. Defeats the purpose.
  • Re-using verifiers across requests. Each authorization request should generate a fresh verifier.

See Also