---
title: "OAuth 2.0 + PKCE Implementation Guide"
url: https://mdfy.app/Oss2bUN8
updated: 2026-04-26T23:16:43.299Z
source: "mdfy.app"
---
# 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

```mermaid
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*