Authorization Code Flow with PKCE
Authorization Code Flow with PKCE
Deep dive into the OAuth 2.1 recommended flow with Proof Key for Code Exchange
Authorization Code Flow with PKCE
The Authorization Code Flow with PKCE (Proof Key for Code Exchange) is the recommended OAuth 2.1 flow for all client types, including public clients like single-page applications (SPAs) and native mobile apps.
What is PKCE?
PKCE (pronounced "pixy") was originally introduced in RFC 7636 as an extension to OAuth 2.0 to protect authorization code interception attacks. In OAuth 2.1, it becomes mandatory for public clients.
PKCE works by having the client generate a secret verifier at the start of the flow and proving possession of that verifier when exchanging the authorization code for tokens. This prevents attackers from using intercepted authorization codes.
How PKCE Works
The PKCE Flow
Step-by-Step Breakdown
Step 1: Generate Code Verifier and Challenge
The client generates two values:
code_verifier: A high-entropy cryptographic random string (43-128 characters)code_challenge: A derived value from the code verifier
javascript1// Generate code_verifier (43-128 characters from URL-safe alphabet) 2function generateCodeVerifier() { 3 const array = new Uint32Array(56); 4 crypto.getRandomValues(array); 5 return base64URLEncode(array); 6} 7 8// Generate code_challenge from verifier 9async function generateCodeChallenge(verifier) { 10 const encoder = new TextEncoder(); 11 const data = encoder.encode(verifier); 12 const digest = await crypto.subtle.digest('SHA-256', data); 13 return base64URLEncode(new Uint8Array(digest)); 14} 15 16// URL-safe Base64 encoding 17function base64URLEncode(buffer) { 18 return btoa(String.fromCharCode(...buffer)) 19 .replace(/\+/g, '-') 20 .replace(/\//g, '_') 21 .replace(/=+$/, ''); 22}
Step 2: Authorization Request
The client redirects the user to the authorization server with the code_challenge and code_challenge_method:
code1https://auth.example.com/authorize? 2 response_type=code& 3 client_id=s6BhdRkqt3& 4 redirect_uri=https%3A%2F%2Fclient.example.com%2Fcallback& 5 scope=openid%20profile%20email& 6 state=af0ifjsldkj& 7 code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM& 8 code_challenge_method=S256
Key parameters:
response_type=code: Requests an authorization codecode_challenge: The derived challenge from the verifiercode_challenge_method=S256: Uses SHA-256 transformation (recommended)state: CSRF protection token (must be validated on callback)
Step 3: User Authentication and Consent
The authorization server:
- Authenticates the user (if not already authenticated)
- Displays a consent screen showing what the application is requesting
- Records the user's consent decision
Step 4: Authorization Code Callback
After user consent, the authorization server redirects back to the client with:
code1https://client.example.com/callback? 2 code=SplxlOBeZQQYbYS6WxSbIA& 3 state=af0ifjsldkj
The client MUST:
- Validate the
stateparameter matches the one sent in the authorization request - Check for error parameters
- Extract the authorization code
Step 5: Token Exchange
The client exchanges the authorization code for tokens, including the code_verifier:
http1POST /token HTTP/1.1 2Host: auth.example.com 3Content-Type: application/x-www-form-urlencoded 4Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW 5 6grant_type=authorization_code& 7code=SplxlOBeZQQYbYS6WxSbIA& 8redirect_uri=https%3A%2F%2Fclient.example.com%2Fcallback& 9client_id=s6BhdRkqt3& 10code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
The authorization server:
- Validates the authorization code
- Verifies the
code_verifierby transforming it and comparing to the storedcode_challenge - Validates the redirect URI matches the original request
- Issues tokens if all validations pass
Step 6: Token Response
json1{ 2 "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", 3 "token_type": "Bearer", 4 "expires_in": 3600, 5 "refresh_token": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4...", 6 "scope": "openid profile email" 7}
Security Benefits of PKCE
Protection Against Authorization Code Interception
Without PKCE, if an attacker intercepts the authorization code (through malware, compromised redirect URIs, or network sniffing), they could exchange it for tokens. With PKCE:
Protection Against CSRF Attacks
The state parameter provides CSRF protection by ensuring the authorization response corresponds to a request initiated by the same client.
Protection Against Mixed-Up Clients
PKCE binds the authorization code to the specific client instance that initiated the flow, preventing code injection attacks between different clients.
Implementation Best Practices
Code Verifier Generation
javascript1// RECOMMENDED: Use cryptographically secure random values 2function generateSecureCodeVerifier() { 3 const length = 128; // Maximum allowed length 4 const charset = 5 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'; 6 const array = new Uint32Array(length); 7 crypto.getRandomValues(array); 8 return Array.from(array, (num) => charset[num % charset.length]).join(''); 9}
State Parameter Management
javascript1// Generate and store state for CSRF protection 2function generateState() { 3 const state = crypto.randomUUID(); 4 sessionStorage.setItem('oauth_state', state); 5 return state; 6} 7 8// Validate state on callback 9function validateState(receivedState) { 10 const storedState = sessionStorage.getItem('oauth_state'); 11 sessionStorage.removeItem('oauth_state'); 12 return storedState === receivedState; 13}
Token Storage
javascript1// Store tokens securely 2async function storeTokens(tokenResponse) { 3 // Access token can be in memory for SPAs 4 sessionStorage.setItem('access_token', tokenResponse.access_token); 5 6 // Refresh token should be stored more securely if supported 7 if (tokenResponse.refresh_token) { 8 // Consider using httpOnly cookies for refresh tokens 9 // or secure storage mechanisms 10 localStorage.setItem('refresh_token', tokenResponse.refresh_token); 11 } 12 13 // Calculate expiration time 14 const expiresAt = Date.now() + tokenResponse.expires_in * 1000; 15 sessionStorage.setItem('token_expires_at', expiresAt.toString()); 16}
Common Implementation Mistakes
1. Weak Code Verifier Generation
javascript1// BAD: Using predictable values 2const codeVerifier = 'my-fixed-verifier'; // NEVER DO THIS 3 4// BAD: Using Math.random() 5const codeVerifier = Math.random().toString(36).substring(2); // NOT SECURE 6 7// GOOD: Using crypto.getRandomValues() 8const array = new Uint32Array(56); 9crypto.getRandomValues(array); 10const codeVerifier = base64URLEncode(array);
2. Not Validating State Parameter
javascript1// BAD: Not checking state parameter 2function handleCallback(code) { 3 // Missing state validation - vulnerable to CSRF 4 exchangeCodeForTokens(code); 5} 6 7// GOOD: Always validate state 8function handleCallback(code, state) { 9 if (!validateState(state)) { 10 throw new Error('Invalid state parameter - possible CSRF attack'); 11 } 12 exchangeCodeForTokens(code); 13}
3. Storing Tokens Insecurely
javascript1// BAD: Storing tokens in URLs or logs 2const url = `https://api.example.com/data?token=${accessToken}`; 3 4// BAD: Storing in localStorage without consideration 5localStorage.setItem('access_token', token); 6 7// BETTER: Use httpOnly cookies for refresh tokens 8// Use memory storage for access tokens in SPAs 9let accessToken = null; 10function setToken(token) { 11 accessToken = token; 12}
Q&A: Authorization Code Flow with PKCE
Q: Why is PKCE mandatory in OAuth 2.1 but was optional in OAuth 2.0?
A: OAuth 2.0 was designed with the assumption that confidential clients could securely store client secrets. However, in practice, many "confidential" clients couldn't actually keep secrets secure (e.g., mobile apps can be reverse-engineered). PKCE provides equivalent security without requiring client secrets, making it suitable for all client types. OAuth 2.1 recognizes this reality and mandates PKCE for all public clients.
Q: Can I use PKCE with confidential clients?
A: Yes, absolutely. While PKCE is mandatory for public clients, confidential clients can also use PKCE in addition to client authentication. This provides defense-in-depth security. Many modern OAuth implementations recommend PKCE for all client types.
Q: What's the difference between S256 and plain code challenge methods?
A: The S256 method applies SHA-256 hashing to the code verifier before Base64URL encoding, while plain uses the verifier directly. OAuth 2.1 requires S256 because it provides better security - even if the code challenge is intercepted, the original verifier cannot be derived from it. The plain method is deprecated and should not be used.
Q: How long should the code verifier be?
A: The OAuth 2.1 specification requires code verifiers to be between 43 and 128 characters. Using the maximum length (128 characters) provides the highest entropy and is recommended for maximum security.
Q: What happens if the code verifier doesn't match during token exchange?
A: The authorization server MUST return an invalid_grant error and MUST NOT issue any tokens. This is the core security mechanism of PKCE - without the correct verifier, the intercepted authorization code is useless.
Q: Can PKCE prevent all types of OAuth attacks?
A: No, PKCE specifically addresses authorization code interception attacks. It does not protect against:
- Phishing attacks
- Token theft after issuance
- Misconfigured redirect URIs
- Insecure token storage
- Server-side vulnerabilities
PKCE should be used as part of a comprehensive security strategy that includes proper redirect URI validation, secure token storage, appropriate token lifetimes, and regular security audits.
Q: How does PKCE work with OpenID Connect?
A: PKCE works seamlessly with OpenID Connect. When using the Authorization Code Flow with PKCE for OIDC, you simply add the code_challenge and code_challenge_method parameters to the authorization request and include the code_verifier in the token request. The ID token is returned along with the access token and refresh token.
Q: Should I implement PKCE myself or use a library?
A: Use a well-tested OAuth library whenever possible. Implementing OAuth flows from scratch is error-prone and can introduce security vulnerabilities. Popular libraries like openid-client (Node.js), AppAuth (Android/iOS), and MSAL (Microsoft) have robust PKCE implementations. Only implement PKCE manually if you have specific requirements that existing libraries cannot meet, and ensure thorough security review.