PKCE (Proof Key for Code Exchange, pronounced "pixie") protects the OAuth authorization code flow when a client secret cannot be securely stored. It's essential for SPAs, mobile apps, and desktop applications.
Key Takeaways
- 1PKCE prevents authorization code interception attacks
- 2Required for public clients (SPAs, mobile, desktop apps)
- 3Uses code_verifier (secret) and code_challenge (hash)
- 4S256 method (SHA-256) is strongly recommended over plain
- 5Generated per-authorization, not reused across sessions
"PKCE is a technique to mitigate against the authorization code interception attack... a dynamically created cryptographically random key called 'code verifier'."— RFC 7636, Section 1
Why PKCE Is Needed
This section covers the specific attacks PKCE prevents. On mobile devices and desktop apps, authorization codes can be intercepted through custom URL scheme hijacking. In browsers, network proxies or malicious extensions might grab codes during redirects.
Without PKCE, authorization codes can be intercepted in several ways. The table below shows common threats and how PKCE neutralizes each one:
| Threat | How It Works | PKCE Protection |
|---|---|---|
| Malicious app intercept | Registers same custom URL scheme | Can't provide correct verifier |
| Network interception | Intercepts code in redirect | Code useless without verifier |
| Cross-app attack | Another app receives redirect | Verifier never leaves client |
The key insight is that even if an attacker intercepts the authorization code, it's useless without the corresponding verifier. The verifier never leaves your client, making code interception a dead end.
PKCE Flow
PKCE adds two parameters to the standard OAuth flow: a secret verifier and its cryptographic hash (challenge). Here's how the pieces fit together:
1. Client generates code_verifier (random string)
2. Client creates code_challenge = SHA256(code_verifier)
3. Client sends code_challenge to authorization server
4. User authenticates, server issues authorization code
5. Client exchanges code + code_verifier for tokens
6. Server verifies SHA256(code_verifier) === code_challengeNotice that the verifier is only revealed at the final step. An attacker who intercepts the code at step 4 still can't complete step 5 because they don't have the verifier.
Implementation
Let's implement PKCE step by step. You'll need functions to generate the verifier, create the challenge, and properly encode both. This implementation works in browsers using the Web Crypto API.
// Generate code verifier (43-128 characters, URL-safe)
function generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64UrlEncode(array);
}
// Generate code challenge (S256 method)
async function generateCodeChallenge(verifier) {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const hash = await crypto.subtle.digest('SHA-256', data);
return base64UrlEncode(new Uint8Array(hash));
}
// Base64 URL encoding
function base64UrlEncode(buffer) {
return btoa(String.fromCharCode(...buffer))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
// Usage
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
// Store codeVerifier securely (sessionStorage for SPAs)
sessionStorage.setItem('pkce_verifier', codeVerifier);The code generates 32 random bytes for the verifier, hashes it with SHA-256 for the challenge, and encodes both as URL-safe base64. Store the verifier in sessionStorage for SPAs—it needs to survive the redirect but shouldn't persist beyond the browser session.
Authorization Request
With PKCE values generated, you include the challenge in your authorization URL. The authorization server stores this challenge and associates it with the code it issues.
https://auth.example.com/authorize?client_id=abc&redirect_uri=https://myapp.com/callback&response_type=code&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256// Build authorization URL with PKCE
const authUrl = new URL('https://auth.example.com/authorize');
authUrl.searchParams.set('client_id', CLIENT_ID);
authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('scope', 'openid profile');
authUrl.searchParams.set('state', generateState());
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
window.location.href = authUrl.href;The two PKCE-specific parameters are code_challenge (the SHA-256 hash) and code_challenge_method (always use S256). Note that you still include the state parameter—PKCE and state protect against different attacks.
Token Exchange
When the user returns with an authorization code, you exchange it for tokens. This is where you prove you initiated the flow by sending the original verifier that matches the challenge.
// Exchange code for tokens (include code_verifier)
async function exchangeCode(code) {
const codeVerifier = sessionStorage.getItem('pkce_verifier');
sessionStorage.removeItem('pkce_verifier'); // Use once
const response = await fetch('https://auth.example.com/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: REDIRECT_URI,
client_id: CLIENT_ID,
code_verifier: codeVerifier, // PKCE: prove we initiated the flow
}),
});
return response.json();
}The authorization server hashes the verifier you send and compares it to the challenge from step 1. If they match, you've proven that you started this flow. Notice the sessionStorage.removeItem call—verifiers are single-use and should be discarded immediately after exchange.
Challenge Methods
OAuth PKCE supports two challenge methods. The method determines how the challenge is derived from the verifier:
| Method | How It Works | Recommendation |
|---|---|---|
| S256 | SHA-256 hash of verifier | Always use this |
| plain | Verifier sent as-is | Only if S256 not supported |
With S256, knowing the challenge doesn't reveal the verifier (SHA-256 is a one-way hash). With plain, the challenge equals the verifier, so intercepting the authorization request exposes everything. That's why S256 is the only recommended method.
Always Use S256
The "plain" method provides minimal security. Always use S256 unless the authorization server doesn't support it (rare). Most modern servers require S256.