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

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

typescript
const { 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

typescript
async 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 S256 method, never plain
  • [ ] Generate verifier with cryptographic randomness (min 43 chars)
  • [ ] Validate state parameter 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