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.

Overview

Why Use PKCE?

Security Benefits

  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 requirements
  3. 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
  4. 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

  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

API Reference

Authorization Endpoint with PKCE

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

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

Token Endpoint with PKCE

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

Usage Examples

JavaScript/TypeScript (Browser)

// 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.
}

Python (CLI Tool)

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()

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

  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 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

References