---
# === IDENTITY ===
id: software/patterns/oauth2-authorization-code-pkce/2026
canonical_question: "How do I implement OAuth2 Authorization Code flow with PKCE?"
aliases:
  - "OAuth2 PKCE flow"
  - "Authorization Code with PKCE"
  - "PKCE implementation"
  - "Proof Key for Code Exchange"
  - "OAuth2 public client authentication"
  - "SPA OAuth2 login flow"
  - "mobile app OAuth2 authorization"
  - "OAuth 2.1 authorization code flow"
entity_type: software_reference
domain: software > patterns > oauth2_authorization_code_pkce
region: global
jurisdiction: global
temporal_scope: 2015-2026

# === VERIFICATION ===
last_verified: 2026-02-24
confidence: 0.92
version: 1.0
first_published: 2026-02-24

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: stable
  last_breaking_change: "OAuth 2.1 draft-14 (2024) mandates PKCE for all authorization code flows; RFC 9700 (2024) BCP for OAuth 2.0 security recommends PKCE universally"
  next_review: 2026-08-23
  change_sensitivity: low

# === CONSTRAINTS ===
constraints:
  - "MUST use S256 (SHA-256) code_challenge_method — 'plain' method is insecure and prohibited by OAuth 2.1"
  - "NEVER embed a client_secret in SPAs, mobile apps, or any public client — PKCE replaces the need for client secrets"
  - "code_verifier MUST be 43-128 characters using unreserved URI characters [A-Z] / [a-z] / [0-9] / '-' / '.' / '_' / '~'"
  - "code_verifier MUST be generated from a cryptographically secure random source (crypto.getRandomValues, os.urandom) — NOT Math.random()"
  - "Always include state parameter for CSRF protection — PKCE protects against code interception, NOT CSRF"
  - "Redirect URIs MUST be exact-match whitelisted on the authorization server — no wildcards"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "Server-to-server authentication with no user interaction"
    use_instead: "software/patterns/oauth2-client-credentials/2026"
  - condition: "Device with no browser (smart TV, CLI tool, IoT)"
    use_instead: "software/patterns/oauth2-device-flow/2026"
  - condition: "Need to verify user identity, not just authorize API access"
    use_instead: "software/patterns/openid-connect-oidc/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: "client_type"
    question: "What type of application are you building?"
    type: choice
    options: ["SPA (React, Vue, Angular)", "Mobile (iOS/Android)", "Desktop (Electron)", "Server-side web app", "CLI tool"]
  - key: "auth_provider"
    question: "Which authorization server / identity provider are you using?"
    type: choice
    options: ["Auth0", "Okta", "Azure AD / Entra ID", "Google", "Keycloak", "Custom OAuth2 server"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/patterns/oauth2-authorization-code-pkce/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-02-24)"

# === RELATED UNITS ===
related_kos:
  related_to:
    - id: "software/patterns/oauth2-client-credentials/2026"
      label: "OAuth2 Client Credentials Flow"
    - id: "software/patterns/oauth2-device-flow/2026"
      label: "OAuth2 Device Authorization Flow"
    - id: "software/patterns/jwt-implementation/2026"
      label: "JWT Token Implementation"
    - id: "software/patterns/session-management/2026"
      label: "Session Management"
  solves:
    - id: "software/patterns/secure-spa-authentication/2026"
      label: "Secure SPA Authentication"
  alternative_to:
    - id: "software/patterns/oauth2-implicit-flow/2026"
      label: "OAuth2 Implicit Flow (deprecated) — PKCE replaces this entirely"
  often_confused_with:
    - id: "software/patterns/openid-connect-oidc/2026"
      label: "OpenID Connect — identity layer on top of OAuth2, not a replacement for PKCE"
    - id: "software/patterns/sso-saml-oidc/2026"
      label: "SSO with SAML/OIDC — enterprise SSO patterns, different scope than PKCE"

# === SOURCES (8 authoritative sources) ===
sources:
  - id: src1
    title: "RFC 7636: Proof Key for Code Exchange by OAuth Public Clients"
    author: IETF
    url: https://datatracker.ietf.org/doc/html/rfc7636
    type: rfc_spec
    published: 2015-09-01
    reliability: authoritative
  - id: src2
    title: "Authorization Code Flow with PKCE"
    author: Auth0
    url: https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow-with-pkce
    type: official_docs
    published: 2025-01-15
    reliability: high
  - id: src3
    title: "Implement Authorization by Grant Type: Authorization Code with PKCE"
    author: Okta
    url: https://developer.okta.com/docs/guides/implement-grant-type/authcodepkce/main/
    type: official_docs
    published: 2025-06-01
    reliability: high
  - id: src4
    title: "The OAuth 2.1 Authorization Framework (draft-ietf-oauth-v2-1-14)"
    author: IETF
    url: https://datatracker.ietf.org/doc/draft-ietf-oauth-v2-1/
    type: rfc_spec
    published: 2024-10-01
    reliability: authoritative
  - id: src5
    title: "RFC 9700: Best Current Practice for OAuth 2.0 Security"
    author: IETF
    url: https://datatracker.ietf.org/doc/rfc9700/
    type: rfc_spec
    published: 2024-12-01
    reliability: authoritative
  - id: src6
    title: "OAuth2 Cheat Sheet"
    author: OWASP
    url: https://cheatsheetseries.owasp.org/cheatsheets/OAuth2_Cheat_Sheet.html
    type: community_resource
    published: 2025-01-01
    reliability: high
  - id: src7
    title: "PKCE for OAuth 2.0"
    author: OAuth.net
    url: https://oauth.net/2/pkce/
    type: community_resource
    published: 2025-01-01
    reliability: high
  - id: src8
    title: "Microsoft identity platform and OAuth 2.0 authorization code flow"
    author: Microsoft
    url: https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow
    type: official_docs
    published: 2025-02-01
    reliability: high
---

# OAuth2 Authorization Code Flow with PKCE

## TL;DR

- **Bottom line**: PKCE (Proof Key for Code Exchange) secures the OAuth2 Authorization Code flow for public clients (SPAs, mobile apps) by binding the authorization request to the token exchange via a cryptographic code verifier/challenge pair, preventing authorization code interception attacks.
- **Key tool/command**: `code_verifier` + `code_challenge = BASE64URL(SHA256(code_verifier))`
- **Watch out for**: Using `plain` instead of `S256` for the code challenge method, or generating the verifier with a non-cryptographic random source.
- **Works with**: Any OAuth2/OIDC provider (Auth0, Okta, Azure AD, Google, Keycloak, custom). Required by OAuth 2.1 for all clients.

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- MUST use S256 (SHA-256) code_challenge_method -- `plain` is insecure and prohibited by OAuth 2.1 [src4]
- NEVER embed a client_secret in SPAs, mobile apps, or any public client -- PKCE replaces the need for client secrets [src1]
- code_verifier MUST be 43-128 characters from unreserved URI character set [A-Z a-z 0-9 - . _ ~] [src1]
- code_verifier MUST come from a cryptographically secure random source -- NOT Math.random() or similar
- Always include the `state` parameter for CSRF protection -- PKCE protects against code interception, not CSRF [src6]
- Redirect URIs MUST be exact-match whitelisted on the authorization server -- never use wildcards [src5]

## Quick Reference

| Step | Action | Endpoint | Key Parameters |
|---|---|---|---|
| 1 | Generate code_verifier | Client-side | 43-128 chars, cryptographically random |
| 2 | Derive code_challenge | Client-side | `BASE64URL(SHA256(code_verifier))` |
| 3 | Authorization request | GET /authorize | `response_type=code`, `code_challenge`, `code_challenge_method=S256`, `state`, `redirect_uri` |
| 4 | User authenticates | Authorization server | User enters credentials, consents |
| 5 | Receive authorization code | Redirect to client | `code` param in callback URL |
| 6 | Exchange code for tokens | POST /token | `grant_type=authorization_code`, `code`, `code_verifier`, `redirect_uri` |
| 7 | Server verifies PKCE | Authorization server | Computes `BASE64URL(SHA256(code_verifier))`, compares to stored challenge |
| 8 | Receive tokens | Client | `access_token`, `refresh_token` (optional), `id_token` (if OIDC) |
| 9 | Use access token | Resource server | `Authorization: Bearer {access_token}` |
| 10 | Refresh tokens | POST /token | `grant_type=refresh_token`, use refresh token rotation for SPAs |

## Decision Tree

```
START
├── Is the client a public client (SPA, mobile, desktop, CLI)?
│   ├── YES → Use Authorization Code + PKCE (this unit)
│   └── NO ↓
├── Is it server-to-server with no user interaction?
│   ├── YES → Use Client Credentials flow
│   └── NO ↓
├── Is it a device without a browser (smart TV, IoT)?
│   ├── YES → Use Device Authorization flow (RFC 8628)
│   └── NO ↓
├── Is it a server-side web app (confidential client)?
│   ├── YES → Use Authorization Code + PKCE + client_secret (OAuth 2.1 recommends PKCE for all)
│   └── NO ↓
└── DEFAULT → Use Authorization Code + PKCE (safest default for any client type)
```

## Step-by-Step Guide

### 1. Generate a cryptographic code_verifier

Create a random string of 43-128 characters. The recommended approach is to generate 32 random bytes and base64url-encode them (yielding 43 characters). [src1]

```javascript
// Generate code_verifier (43 chars from 32 random bytes)
const array = new Uint8Array(32);
crypto.getRandomValues(array);
const codeVerifier = btoa(String.fromCharCode(...array))
  .replace(/\+/g, '-')
  .replace(/\//g, '_')
  .replace(/=+$/, '');
```

**Verify**: `codeVerifier.length` -> should be 43, all characters match `[A-Za-z0-9\-._~]`

### 2. Derive the code_challenge using S256

Hash the code_verifier with SHA-256 and base64url-encode the result. [src1]

```javascript
// Derive code_challenge = BASE64URL(SHA256(code_verifier))
async function generateCodeChallenge(verifier) {
  const encoder = new TextEncoder();
  const data = encoder.encode(verifier);
  const digest = await crypto.subtle.digest('SHA-256', data);
  return btoa(String.fromCharCode(...new Uint8Array(digest)))
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '');
}
const codeChallenge = await generateCodeChallenge(codeVerifier);
```

**Verify**: `codeChallenge` should be a 43-character base64url string, different from codeVerifier

### 3. Build the authorization URL and redirect

Construct the authorization request with PKCE parameters, state, and redirect URI. [src2]

```javascript
// Build authorization URL
const state = crypto.randomUUID(); // CSRF protection
sessionStorage.setItem('oauth_state', state);
sessionStorage.setItem('code_verifier', codeVerifier);

const params = new URLSearchParams({
  response_type: 'code',
  client_id: 'YOUR_CLIENT_ID',
  redirect_uri: 'https://app.example.com/callback',
  scope: 'openid profile email',
  state: state,
  code_challenge: codeChallenge,
  code_challenge_method: 'S256'
});

window.location.href = `https://auth.example.com/authorize?${params}`;
```

**Verify**: URL contains `code_challenge` and `code_challenge_method=S256` parameters

### 4. Handle the callback and extract the authorization code

When the authorization server redirects back, validate the state parameter and extract the code. [src2]

```javascript
// In callback handler (e.g., /callback route)
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const returnedState = urlParams.get('state');

// Validate state to prevent CSRF
const savedState = sessionStorage.getItem('oauth_state');
if (returnedState !== savedState) {
  throw new Error('State mismatch — possible CSRF attack');
}
```

**Verify**: `code` is present and `returnedState === savedState`

### 5. Exchange the authorization code for tokens

Send the code and the original code_verifier to the token endpoint. The server computes SHA256(code_verifier) and compares it to the stored code_challenge. [src1]

```javascript
// Exchange code for tokens
const codeVerifier = sessionStorage.getItem('code_verifier');

const tokenResponse = await fetch('https://auth.example.com/oauth/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    client_id: 'YOUR_CLIENT_ID',
    code: code,
    redirect_uri: 'https://app.example.com/callback',
    code_verifier: codeVerifier
  })
});

const tokens = await tokenResponse.json();
// tokens.access_token, tokens.refresh_token, tokens.id_token
```

**Verify**: Response contains `access_token`. HTTP 200 status. No `invalid_grant` error.

### 6. Store tokens securely and clean up

Store tokens appropriately for your platform. For SPAs, use in-memory storage or httpOnly cookies -- never localStorage. [src6]

```javascript
// Store tokens in memory (SPA best practice)
// NEVER use localStorage for tokens in SPAs
const tokenStore = {
  accessToken: tokens.access_token,
  refreshToken: tokens.refresh_token,
  expiresAt: Date.now() + (tokens.expires_in * 1000)
};

// Clean up PKCE artifacts
sessionStorage.removeItem('code_verifier');
sessionStorage.removeItem('oauth_state');
```

**Verify**: `sessionStorage` no longer contains `code_verifier` or `oauth_state`

## Code Examples
<!-- Keep inline examples <=15 lines. For longer scripts, extract to scripts/ subdirectory
     and link: "Full script: [name.ext](scripts/name.ext) (N lines)" -->

### JavaScript/TypeScript: SPA PKCE Helper

```javascript
// Input:  Authorization server URLs + client_id
// Output: Complete PKCE flow utilities

class PKCEClient {
  constructor(authUrl, tokenUrl, clientId, redirectUri) {
    this.authUrl = authUrl;
    this.tokenUrl = tokenUrl;
    this.clientId = clientId;
    this.redirectUri = redirectUri;
  }

  async generateVerifier() {
    const bytes = crypto.getRandomValues(new Uint8Array(32));
    return this.base64url(bytes);
  }

  async generateChallenge(verifier) {
    const digest = await crypto.subtle.digest(
      'SHA-256', new TextEncoder().encode(verifier)
    );
    return this.base64url(new Uint8Array(digest));
  }

  base64url(bytes) {
    return btoa(String.fromCharCode(...bytes))
      .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
  }
}
```

### Python: Server-Side PKCE Flow

> Full script: [python-server-side-pkce-flow.py](scripts/python-server-side-pkce-flow.py) (35 lines)

```python
# Input:  auth_url, token_url, client_id
# Output: PKCE authorization + token exchange
import hashlib, base64, os, secrets, urllib.parse
import httpx  # pip install httpx>=0.27
def generate_pkce_pair():
# ... (see full script)
```

### Node.js/Express: Backend Callback Handler

> Full script: [node-js-express-backend-callback-handler.js](scripts/node-js-express-backend-callback-handler.js) (32 lines)

```javascript
// Input:  Express request with ?code=...&state=...
// Output: Token response from authorization server
const crypto = require('node:crypto');
function generatePKCE() {
  const verifier = crypto.randomBytes(32)
# ... (see full script)
```

## Anti-Patterns

### Wrong: Using `plain` code challenge method

```javascript
// BAD -- plain method sends code_verifier as code_challenge
// An attacker who intercepts the authorization request gets the verifier
const params = new URLSearchParams({
  code_challenge: codeVerifier,         // verifier sent in the clear
  code_challenge_method: 'plain'        // no hashing
});
```

### Correct: Using S256 code challenge method

```javascript
// GOOD -- S256 hashes the verifier so intercepting the challenge is useless
const challenge = await generateCodeChallenge(codeVerifier);
const params = new URLSearchParams({
  code_challenge: challenge,            // SHA-256 hash, not the verifier
  code_challenge_method: 'S256'         // always S256
});
```

### Wrong: Using Math.random() for code_verifier

```javascript
// BAD -- Math.random() is not cryptographically secure
// Attackers can predict the sequence
const verifier = Array.from({length: 43}, () =>
  'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
  [Math.floor(Math.random() * 62)]
).join('');
```

### Correct: Using crypto.getRandomValues()

```javascript
// GOOD -- cryptographically secure random bytes
const bytes = crypto.getRandomValues(new Uint8Array(32));
const verifier = btoa(String.fromCharCode(...bytes))
  .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
```

### Wrong: Storing tokens in localStorage

```javascript
// BAD -- localStorage is accessible to any JS on the page (XSS attack vector)
localStorage.setItem('access_token', tokens.access_token);
localStorage.setItem('refresh_token', tokens.refresh_token);
```

### Correct: Storing tokens in memory or httpOnly cookies

```javascript
// GOOD -- in-memory storage, cleared on page close
// For persistence, use httpOnly secure cookies set by backend
let tokenStore = { accessToken: null, refreshToken: null };
tokenStore.accessToken = tokens.access_token;
// Or: backend sets httpOnly cookie with SameSite=Strict
```

### Wrong: Omitting state parameter (CSRF vulnerability)

```javascript
// BAD -- no state parameter means no CSRF protection
const params = new URLSearchParams({
  response_type: 'code',
  client_id: CLIENT_ID,
  redirect_uri: REDIRECT_URI,
  code_challenge: challenge,
  code_challenge_method: 'S256'
  // missing: state parameter
});
```

### Correct: Including cryptographic state parameter

```javascript
// GOOD -- state parameter prevents CSRF attacks
const state = crypto.randomUUID();
sessionStorage.setItem('oauth_state', state);
const params = new URLSearchParams({
  response_type: 'code',
  client_id: CLIENT_ID,
  redirect_uri: REDIRECT_URI,
  code_challenge: challenge,
  code_challenge_method: 'S256',
  state: state  // verify this on callback
});
```

## Common Pitfalls

- **code_verifier not stored between redirect**: The verifier generated before redirect must survive the round-trip. In SPAs use `sessionStorage`; in server apps use the session. If lost, token exchange fails with `invalid_grant`. Fix: Store verifier in `sessionStorage` (SPA) or server session before redirect. [src2]
- **Incorrect base64url encoding**: Standard base64 uses `+`, `/`, and `=` padding. OAuth2 PKCE requires base64url: `+` -> `-`, `/` -> `_`, strip `=` padding. Fix: Apply `.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')` after btoa(). [src1]
- **Redirect URI mismatch**: The redirect_uri in the token request must exactly match the one in the authorization request, character for character. Fix: Use the same constant for both requests; no trailing slashes. [src3]
- **Missing CORS headers on token endpoint**: SPAs making direct POST to `/token` may hit CORS errors. Fix: Ensure authorization server allows CORS from your app origin, or proxy through your backend. [src6]
- **Not using refresh token rotation for SPAs**: Long-lived refresh tokens in SPAs are dangerous if leaked. Fix: Enable refresh token rotation (each use issues a new refresh token and invalidates the old one). [src2]
- **PKCE downgrade attack**: An attacker strips the code_challenge from the authorization request. Fix: Configure the authorization server to require PKCE (reject requests without code_challenge). [src5]
- **Using the Implicit flow instead of PKCE**: The Implicit flow (response_type=token) exposes tokens in the URL fragment. Fix: Always use Authorization Code + PKCE; Implicit is deprecated in OAuth 2.1. [src4]

## Diagnostic Commands

```bash
# Test code_verifier generation (Node.js)
node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"

# Verify S256 challenge computation
echo -n "YOUR_CODE_VERIFIER" | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '='

# Test token endpoint manually with curl
curl -X POST https://auth.example.com/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "code=AUTHORIZATION_CODE" \
  -d "code_verifier=YOUR_CODE_VERIFIER" \
  -d "redirect_uri=https://app.example.com/callback"

# Decode a JWT access token to inspect claims
echo "ACCESS_TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | python3 -m json.tool

# Check if authorization server supports PKCE (OpenID Discovery)
curl -s https://auth.example.com/.well-known/openid-configuration | python3 -c "import sys,json; d=json.load(sys.stdin); print('PKCE methods:', d.get('code_challenge_methods_supported', 'not advertised'))"
```

## Version History & Compatibility

| Version | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| OAuth 2.1 (draft-14, 2024) | Draft / De facto standard | PKCE mandatory for ALL authorization code flows; Implicit flow removed | Add code_challenge + code_verifier to all existing auth code flows |
| RFC 9700 (2024) | BCP (Best Current Practice) | Recommends PKCE for all clients including confidential | Add PKCE even if you have a client_secret |
| RFC 7636 (2015) | Standard | Original PKCE specification | Baseline implementation; supports S256 and plain methods |
| OAuth 2.0 (RFC 6749, 2012) | Standard | PKCE not part of core spec | PKCE was an extension; Implicit flow was the recommended SPA approach |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Building a SPA (React, Vue, Angular) | Server-to-server API calls (no user) | Client Credentials flow (RFC 6749 sec 4.4) |
| Building a mobile app (iOS, Android) | Device with no browser/keyboard | Device Authorization flow (RFC 8628) |
| Building a desktop app (Electron) | You only need user identity, not API access | OpenID Connect (but OIDC uses PKCE under the hood) |
| Building a server-side web app (OAuth 2.1 recommends it) | Issuing your own tokens (you are the auth server) | Build token endpoint with proper PKCE verification |
| Any new OAuth2 implementation (future-proofing for 2.1) | Simple API key authentication suffices | API keys with rate limiting |

## Important Caveats

- OAuth 2.1 is still in draft status (draft-14 as of late 2024), but major providers (Auth0, Okta, Azure AD, Google) already enforce or strongly recommend PKCE for all clients. Treat it as the de facto standard.
- PKCE protects against authorization code interception only. It does NOT protect against: XSS (use CSP + input sanitization), CSRF (use `state` parameter), or token leakage from the client (use secure storage).
- Some older authorization servers may not support PKCE. Check the `code_challenge_methods_supported` field in the provider's OpenID Discovery document (`.well-known/openid-configuration`).
- For SPAs, the current best practice is to use a Backend-for-Frontend (BFF) pattern where tokens are stored server-side in an httpOnly cookie session, rather than handling tokens directly in the browser. PKCE is still used in the BFF's communication with the authorization server.
- Refresh token rotation is essential for SPAs using PKCE. Without it, a stolen refresh token grants indefinite access. Enable automatic rotation and absolute lifetime limits on the authorization server.

## Related Units
<!-- Generated from related_kos frontmatter -->

- [OAuth2 Client Credentials Flow](/software/patterns/oauth2-client-credentials/2026)
- [OAuth2 Device Authorization Flow](/software/patterns/oauth2-device-flow/2026)
- [JWT Token Implementation](/software/patterns/jwt-implementation/2026)
- [Session Management](/software/patterns/session-management/2026)
- [SSO with SAML/OIDC](/software/patterns/sso-saml-oidc/2026)
