OAuth 2.0 + PKCE Implementation Guide
Why PKCE?
PKCE (Proof Key for Code Exchange) prevents authorization code interception attacks. It's mandatory for public clients (SPAs, mobile apps, CLI tools) and recommended for all OAuth flows since 2023.
Flow Diagram
sequenceDiagram
participant User
participant App
participant AuthServer
participant API
App->>App: Generate code_verifier (random)
App->>App: code_challenge = SHA256(code_verifier)
App->>AuthServer: /authorize?code_challenge=...&method=S256
AuthServer->>User: Login prompt
User->>AuthServer: Credentials
AuthServer->>App: Authorization code
App->>AuthServer: /token + code + code_verifier
AuthServer->>AuthServer: Verify SHA256(code_verifier) == code_challenge
AuthServer->>App: Access token + Refresh token
App->>API: Request + Bearer token
API->>App: Protected resource
Implementation
1. Generate PKCE Values
typescriptasync function generatePKCE() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
const verifier = base64url(array);
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const hash = await crypto.subtle.digest("SHA-256", data);
const challenge = base64url(new Uint8Array(hash));
return { verifier, challenge };
}
function base64url(bytes: Uint8Array): string {
return btoa(String.fromCharCode(...bytes))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
}
2. Authorization Request
typescriptconst { verifier, challenge } = await generatePKCE();
// Store verifier for later
sessionStorage.setItem("pkce_verifier", verifier);
const params = new URLSearchParams({
response_type: "code",
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
scope: "openid profile email",
state: crypto.randomUUID(),
code_challenge: challenge,
code_challenge_method: "S256",
});
window.location.href = `${AUTH_URL}/authorize?${params}`;
3. Token Exchange
typescriptasync function exchangeCode(code: string): Promise<TokenResponse> {
const verifier = sessionStorage.getItem("pkce_verifier");
const res = await fetch(`${AUTH_URL}/token`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
client_id: CLIENT_ID,
code,
redirect_uri: REDIRECT_URI,
code_verifier: verifier!,
}),
});
sessionStorage.removeItem("pkce_verifier");
return res.json();
}
Security Checklist
- [x] Use
S256method, neverplain - [ ] Generate verifier with cryptographic randomness (min 43 chars)
- [ ] Validate
stateparameter to prevent CSRF - [x] Store tokens securely (httpOnly cookies or encrypted storage)
- [x] Implement token rotation on refresh
- [ ] Add DPoP binding for token theft protection (optional, advanced)
References: RFC 7636, RFC 9126, OAuth 2.1 Draft