Skip to content

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.

  1. Authorization Code Interception Protection

    • Prevents authorization code theft by malicious apps
    • Protects against code injection attacks
    • Essential for public clients (no client secret)
  2. Mobile App Security

    • Protects against malicious apps with same custom URI scheme
    • Works with app-to-app redirects
    • Compatible with OAuth 2.1-style requirements
  3. Single Page Application (SPA) Security

    • Eliminates need for client secret in browser
    • Protects against XSS attacks stealing codes
    • Required by current OAuth 2.1 drafts for authorization code flows
  4. OAuth 2.1 Draft Alignment

    • PKCE is required by current OAuth 2.1 drafts for authorization code flows
    • Recommended for all clients (including confidential)
    • Future-proof security
  • 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)
  1. Generate code_verifier: Client creates a cryptographically random string (43-128 chars)
  2. Calculate code_challenge: BASE64URL(SHA-256(code_verifier))
  3. Authorization Request: Include code_challenge and code_challenge_method=S256
  4. Token Request: Include original code_verifier
  5. Verification: Server validates SHA-256(code_verifier) == code_challenge

GET/POST /authorize

ParameterRequiredDescription
code_challengeYes*Base64url-encoded SHA-256 hash of code_verifier
code_challenge_methodYes*Must be S256 (SHA-256)
response_typeYesMust be code
client_idYesClient identifier
redirect_uriYesCallback URI
scopeYesRequested scopes
stateRecommendedCSRF protection

*Required for public clients, recommended for all clients

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=S256
Host: auth.example.com

POST /token

ParameterRequiredDescription
grant_typeYesMust be authorization_code
codeYesAuthorization code from /authorize
redirect_uriYesSame redirect_uri from /authorize
client_idYesClient identifier
code_verifierYes*Original random string (43-128 chars)

*Required if code_challenge was provided in authorization request

// Step 1: Generate code_verifier
function 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_challenge
async 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 flow
async 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 tokens
async 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.
}
import hashlib
import base64
import secrets
from urllib.parse import urlencode
import 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()
  • Length: 43-128 characters
  • Characters: [A-Z], [a-z], [0-9], -, ., _, ~
  • Entropy: Minimum 256 bits recommended
  • Generation: Use cryptographically secure random number generator

Authrim supports:

  • S256 (SHA-256) - Recommended and default
  • plain - Not supported (insecure, deprecated)
  1. code_verifier Security

    • Use cryptographically secure random number generator
    • Store securely (sessionStorage, secure enclave)
    • Never log or expose code_verifier
    • Delete after use
  2. Public Clients

    • PKCE is mandatory for mobile apps, SPAs, desktop apps, CLI tools
  3. Confidential Clients

    • PKCE is recommended for all clients (defense-in-depth)
{
"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"
}
  • OAuth 2.1 draft alignment: PKCE is required for authorization code flows
  • FAPI 2.0: PKCE mandatory, S256 method required