Authorization Code + PKCE
RFC 7636Client type: Public clients: single-page apps (SPA), mobile apps, CLI tools that cannot securely store a client secret
PKCE (Proof Key for Code Exchange, RFC 7636) extends Authorization Code for public clients. The client generates a cryptographically random code_verifier, computes code_challenge = BASE64URL(SHA256(verifier)), and sends the challenge in the authorization request. At token exchange, the verifier is sent instead of a client secret – proving the same client initiated both steps.
How it works
PKCE was designed to prevent authorization code interception attacks in public clients (apps that cannot keep a client secret).
Problem PKCE solves: Mobile apps and SPAs cannot securely store a client_secret. Without PKCE, an attacker can register a malicious app with the same redirect URI scheme, intercept the authorization code, and exchange it for tokens.
PKCE mechanism: 1. Client generates code_verifier: a cryptographically random string (43–128 characters, unreserved characters) 2. Client computes code_challenge = BASE64URL(SHA256(code_verifier)) using S256 method 3. Authorization request includes code_challenge and code_challenge_method=S256 4. Token exchange includes code_verifier (the original secret) 5. Authorization server verifies: SHA256(code_verifier) == code_challenge
Security guarantee: an attacker who intercepts the authorization code cannot exchange it for tokens because they don't have the code_verifier (it was never transmitted, only the hash was).
OAuth 2.1 mandates PKCE for all authorization code flows, including confidential clients. PKCE adds defense-in-depth even for server-side apps.
S256 only: the plain method (code_challenge = code_verifier) is deprecated. Always use S256.
Flow Steps
- 1
Generate code_verifier: crypto.randomBytes(32) → base64url encode (43–128 chars)
- 2
Compute code_challenge: base64url(sha256(code_verifier))
- 3
Redirect to /authorize with code_challenge and code_challenge_method=S256
- 4
User authenticates; authorization server stores challenge
- 5
Callback with code; client POSTs code + code_verifier to /token (no client_secret)
- 6
Server verifies SHA256(code_verifier) == stored code_challenge, issues tokens
Parameters
| Parameter | Required | Description |
|---|---|---|
| code_verifier | Yes | 43–128 char random string. Unreserved chars [A-Z a-z 0-9 - . _ ~]. Generated fresh per authorization request. |
| code_challenge | Yes | BASE64URL(SHA256(ASCII(code_verifier))). Sent in authorization request. |
| code_challenge_method | Yes | Must be S256. plain is deprecated and must not be used. |
Examples
// Generate code_verifier
function generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return btoa(String.fromCharCode(...array))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
// Compute code_challenge
async function generateCodeChallenge(verifier) {
const data = new TextEncoder().encode(verifier);
const digest = await crypto.subtle.digest('SHA-256', data);
return btoa(String.fromCharCode(...new Uint8Array(digest)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
// Authorization request
const verifier = generateCodeVerifier();
const challenge = await generateCodeChallenge(verifier);
sessionStorage.setItem('pkce_verifier', verifier);
window.location.href = `https://auth.example.com/authorize?
response_type=code&client_id=spa_abc
&code_challenge=${challenge}&code_challenge_method=S256`;
// Token exchange (no client_secret, use verifier instead)
const response = await fetch('/oauth/token', {
method: 'POST',
body: new URLSearchParams({
grant_type: 'authorization_code',
code: authCode,
code_verifier: sessionStorage.getItem('pkce_verifier'),
client_id: 'spa_abc'
})
});When to use
All browser-based SPAs, all mobile apps (iOS, Android), CLI tools, and any public client that cannot store a client_secret. Also recommended for confidential clients as defense-in-depth (OAuth 2.1 mandates it for all flows).