PKCE
PKCE (RFC 7636) is a critical security extension that protects authorization codes from interception attacks, especially for public clients like mobile apps and SPAs that cannot securely store client secrets.
Overview
- RFC: RFC 7636 - Proof Key for Code Exchange by OAuth Public Clients
- Status: Fully Implemented
- Method:
S256(SHA-256) - Recommended and enforced - Supported Flows: Authorization Code Flow
Why Use PKCE?
Security Benefits
-
Authorization Code Interception Protection
- Prevents authorization code theft by malicious apps
- Protects against code injection attacks
- Essential for public clients (no client secret)
-
Mobile App Security
- Protects against malicious apps with same custom URI scheme
- Works with app-to-app redirects
- Compatible with OAuth 2.1 requirements
-
Single Page Application (SPA) Security
- Eliminates need for client secret in browser
- Protects against XSS attacks stealing codes
- Part of OAuth 2.1 mandatory requirements
-
OAuth 2.1 Compliance
- PKCE is mandatory in OAuth 2.1
- Recommended for all clients (including confidential)
- Future-proof security
Use Cases
- Mobile Apps (iOS, Android, React Native)
- Single Page Applications (React, Vue, Angular)
- Desktop Applications (Electron)
- CLI Tools
- IoT Devices
- Public Clients (any client that cannot store secrets securely)
How PKCE Works
Flow Overview
- Generate code_verifier: Client creates a cryptographically random string (43-128 chars)
- Calculate code_challenge:
BASE64URL(SHA-256(code_verifier)) - Authorization Request: Include
code_challengeandcode_challenge_method=S256 - Token Request: Include original
code_verifier - Verification: Server validates
SHA-256(code_verifier) == code_challenge
API Reference
Authorization Endpoint with PKCE
GET/POST /authorize
| Parameter | Required | Description |
|---|---|---|
code_challenge | Yes* | Base64url-encoded SHA-256 hash of code_verifier |
code_challenge_method | Yes* | Must be S256 (SHA-256) |
response_type | Yes | Must be code |
client_id | Yes | Client identifier |
redirect_uri | Yes | Callback URI |
scope | Yes | Requested scopes |
state | Recommended | CSRF protection |
*Required for public clients, recommended for all clients
Example Request
GET /authorize ?response_type=code &client_id=my_client_id &redirect_uri=https://myapp.example.com/callback &scope=openid+profile+email &state=abc123 &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM &code_challenge_method=S256Host: auth.example.comToken Endpoint with PKCE
POST /token
| Parameter | Required | Description |
|---|---|---|
grant_type | Yes | Must be authorization_code |
code | Yes | Authorization code from /authorize |
redirect_uri | Yes | Same redirect_uri from /authorize |
client_id | Yes | Client identifier |
code_verifier | Yes* | Original random string (43-128 chars) |
*Required if code_challenge was provided in authorization request
Usage Examples
JavaScript/TypeScript (Browser)
// Step 1: Generate code_verifierfunction generateCodeVerifier(): string { const array = new Uint8Array(32); crypto.getRandomValues(array); return base64urlEncode(array);}
function base64urlEncode(buffer: Uint8Array): string { const base64 = btoa(String.fromCharCode(...buffer)); return base64 .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, '');}
// Step 2: Calculate code_challengeasync function generateCodeChallenge(verifier: string): Promise<string> { const encoder = new TextEncoder(); const data = encoder.encode(verifier); const hashBuffer = await crypto.subtle.digest('SHA-256', data); return base64urlEncode(new Uint8Array(hashBuffer));}
// Step 3: Start authorization flowasync function startAuthorization() { const codeVerifier = generateCodeVerifier(); const codeChallenge = await generateCodeChallenge(codeVerifier);
// Store code_verifier for later use sessionStorage.setItem('code_verifier', codeVerifier);
// Build authorization URL const authUrl = new URL('https://auth.example.com/authorize'); authUrl.searchParams.set('response_type', 'code'); authUrl.searchParams.set('client_id', 'my_client_id'); authUrl.searchParams.set('redirect_uri', 'https://myapp.example.com/callback'); authUrl.searchParams.set('scope', 'openid profile email'); authUrl.searchParams.set('state', generateRandomState()); authUrl.searchParams.set('code_challenge', codeChallenge); authUrl.searchParams.set('code_challenge_method', 'S256');
window.location.href = authUrl.toString();}
// Step 4: Handle callback and exchange code for tokensasync function handleCallback() { const params = new URLSearchParams(window.location.search); const code = params.get('code'); const codeVerifier = sessionStorage.getItem('code_verifier');
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: 'https://myapp.example.com/callback', client_id: 'my_client_id', code_verifier: codeVerifier!, }), });
const tokens = await response.json(); // tokens.access_token, tokens.id_token, etc.}Python (CLI Tool)
import hashlibimport base64import secretsfrom urllib.parse import urlencodeimport requests
def generate_code_verifier(): """Generate cryptographically secure code_verifier""" return base64.urlsafe_b64encode(secrets.token_bytes(32)).decode('utf-8').rstrip('=')
def generate_code_challenge(verifier): """Calculate code_challenge from verifier""" digest = hashlib.sha256(verifier.encode('utf-8')).digest() return base64.urlsafe_b64encode(digest).decode('utf-8').rstrip('=')
def start_oauth_flow(): code_verifier = generate_code_verifier() code_challenge = generate_code_challenge(code_verifier)
auth_params = { 'response_type': 'code', 'client_id': 'my_cli_client_id', 'redirect_uri': 'http://localhost:8080/callback', 'scope': 'openid profile email', 'code_challenge': code_challenge, 'code_challenge_method': 'S256', }
auth_url = f'https://auth.example.com/authorize?{urlencode(auth_params)}' # Open browser and wait for callback...
# Exchange code for tokens token_response = requests.post( 'https://auth.example.com/token', data={ 'grant_type': 'authorization_code', 'code': authorization_code, 'redirect_uri': 'http://localhost:8080/callback', 'client_id': 'my_cli_client_id', 'code_verifier': code_verifier, } ) return token_response.json()Implementation Details
code_verifier Requirements
- Length: 43-128 characters
- Characters:
[A-Z],[a-z],[0-9],-,.,_,~ - Entropy: Minimum 256 bits recommended
- Generation: Use cryptographically secure random number generator
Challenge Method
Authrim supports:
- S256 (SHA-256) - Recommended and default
plain- Not supported (insecure, deprecated)
Security Considerations
-
code_verifier Security
- Use cryptographically secure random number generator
- Store securely (sessionStorage, secure enclave)
- Never log or expose code_verifier
- Delete after use
-
Public Clients
- PKCE is mandatory for mobile apps, SPAs, desktop apps, CLI tools
-
Confidential Clients
- PKCE is recommended for all clients (defense-in-depth)
Error Responses
{ "error": "invalid_request", "error_description": "code_verifier is required when code_challenge was provided"}{ "error": "invalid_grant", "error_description": "code_verifier does not match code_challenge"}Compliance
- OAuth 2.1: PKCE mandatory for all authorization code flows
- FAPI 2.0: PKCE mandatory, S256 method required