PKCE Explained
Proof Key for Code Exchange (PKCE) - Enhanced OAuth security.
What is PKCE?
PKCE (pronounced "pixy") is a security extension to OAuth 2.0 that protects authorization code interception attacks.
Defined in RFC 7636.
The Problem
Without PKCE, authorization codes can be intercepted:
1. User starts login
2. App redirects to auth server with code request
3. User authenticates
4. Auth server returns code to redirect URI
↓
⚠️ Attacker intercepts code
↓
5. Attacker exchanges code for tokens
6. Attacker gains access
The Solution
PKCE adds a secret known only to the original client:
1. App generates random code_verifier
2. App calculates code_challenge = sha256(code_verifier)
3. App stores code_verifier locally
4. App sends code_challenge to auth server
5. User authenticates
6. Auth server returns authorization code
7. App exchanges code + code_verifier for tokens
8. Auth server verifies sha256(code_verifier) == code_challenge
9. Tokens issued only if verification passes
If an attacker intercepts the code, they can't exchange it without the code_verifier.
How PKCE Works
Step 1: Generate Code Verifier
A random, high-entropy string:
function generateCodeVerifier(): string {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64url(array);
}
// Example output:
// "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
Step 2: Create Code Challenge
SHA-256 hash of the verifier:
async function generateCodeChallenge(verifier: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const hash = await crypto.subtle.digest('SHA-256', data);
return base64url(new Uint8Array(hash));
}
// Example output:
// "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
Step 3: Authorization Request
Include challenge in the request:
GET /authorize?
response_type=code
&client_id=xxx
&redirect_uri=https://app.com/callback
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
&state=random123
Step 4: Token Exchange
Include verifier in exchange:
POST /oauth/token
grant_type=authorization_code
&code=received_code
&redirect_uri=https://app.com/callback
&client_id=xxx
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
Step 5: Server Verification
Server calculates sha256(code_verifier) and compares to stored code_challenge:
sha256("dBjftJeZ...") == "E9Melhoa2O..."
↓
MATCH ✓
↓
Issue tokens
Challenge Methods
| Method | Description | Recommended |
|---|---|---|
S256 | SHA-256 hash | ✅ Yes |
plain | No hashing | ❌ No |
Always use S256 (SHA-256).
SDK Implementation
The SyAuth SDK handles PKCE automatically:
// All of this happens automatically:
// 1. Generate code_verifier
// 2. Calculate code_challenge
// 3. Store verifier in secure cookie
// 4. Send challenge in auth request
// 5. Retrieve verifier for token exchange
loginWithRedirect(); // PKCE is automatic
Security Benefits
| Attack | Without PKCE | With PKCE |
|---|---|---|
| Code interception | Vulnerable | Protected |
| Man-in-the-middle | Vulnerable | Protected |
| Malicious app | Vulnerable | Protected |
| Replay attacks | Possible | Prevented |
Why Not Just Client Secret?
Client secrets don't work for:
- Public clients (mobile apps, SPAs) - can't securely store secrets
- Native apps - secrets can be extracted from binary
PKCE works for ALL client types.
Common Issues
"Invalid code_verifier"
Causes:
- Verifier lost between requests
- Different client making token request
- Verifier corrupted in storage
Solution:
- Ensure verifier is stored in secure, persistent storage
- Same client instance must complete the flow
"Code already used"
Authorization codes are one-time use. You cannot retry the token exchange.
Next Steps
- Token Security - Secure token handling
- OAuth 2.0 with PKCE - Implementation guide