Skip to main content

Authorization Code + PKCE

RFC 7636

Client type: Public clients: single-page apps (SPA), mobile apps, CLI tools that cannot securely store a client secret

PKCE (Proof Key for Code Exchange, RFC 7636) extends Authorization Code for public clients. The client generates a cryptographically random code_verifier, computes code_challenge = BASE64URL(SHA256(verifier)), and sends the challenge in the authorization request. At token exchange, the verifier is sent instead of a client secret – proving the same client initiated both steps.

How it works

PKCE was designed to prevent authorization code interception attacks in public clients (apps that cannot keep a client secret).

Problem PKCE solves: Mobile apps and SPAs cannot securely store a client_secret. Without PKCE, an attacker can register a malicious app with the same redirect URI scheme, intercept the authorization code, and exchange it for tokens.

PKCE mechanism: 1. Client generates code_verifier: a cryptographically random string (43–128 characters, unreserved characters) 2. Client computes code_challenge = BASE64URL(SHA256(code_verifier)) using S256 method 3. Authorization request includes code_challenge and code_challenge_method=S256 4. Token exchange includes code_verifier (the original secret) 5. Authorization server verifies: SHA256(code_verifier) == code_challenge

Security guarantee: an attacker who intercepts the authorization code cannot exchange it for tokens because they don't have the code_verifier (it was never transmitted, only the hash was).

OAuth 2.1 mandates PKCE for all authorization code flows, including confidential clients. PKCE adds defense-in-depth even for server-side apps.

S256 only: the plain method (code_challenge = code_verifier) is deprecated. Always use S256.

Flow Steps

  1. 1

    Generate code_verifier: crypto.randomBytes(32) → base64url encode (43–128 chars)

  2. 2

    Compute code_challenge: base64url(sha256(code_verifier))

  3. 3

    Redirect to /authorize with code_challenge and code_challenge_method=S256

  4. 4

    User authenticates; authorization server stores challenge

  5. 5

    Callback with code; client POSTs code + code_verifier to /token (no client_secret)

  6. 6

    Server verifies SHA256(code_verifier) == stored code_challenge, issues tokens

Parameters

ParameterRequiredDescription
code_verifierYes43–128 char random string. Unreserved chars [A-Z a-z 0-9 - . _ ~]. Generated fresh per authorization request.
code_challengeYesBASE64URL(SHA256(ASCII(code_verifier))). Sent in authorization request.
code_challenge_methodYesMust be S256. plain is deprecated and must not be used.

Examples

PKCE implementation (JavaScript)
// Generate code_verifier
function generateCodeVerifier() {
  const array = new Uint8Array(32);
  crypto.getRandomValues(array);
  return btoa(String.fromCharCode(...array))
    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}

// Compute code_challenge
async function generateCodeChallenge(verifier) {
  const data = new TextEncoder().encode(verifier);
  const digest = await crypto.subtle.digest('SHA-256', data);
  return btoa(String.fromCharCode(...new Uint8Array(digest)))
    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}

// Authorization request
const verifier = generateCodeVerifier();
const challenge = await generateCodeChallenge(verifier);
sessionStorage.setItem('pkce_verifier', verifier);

window.location.href = `https://auth.example.com/authorize?
  response_type=code&client_id=spa_abc
  &code_challenge=${challenge}&code_challenge_method=S256`;

// Token exchange (no client_secret, use verifier instead)
const response = await fetch('/oauth/token', {
  method: 'POST',
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    code: authCode,
    code_verifier: sessionStorage.getItem('pkce_verifier'),
    client_id: 'spa_abc'
  })
});

When to use

All browser-based SPAs, all mobile apps (iOS, Android), CLI tools, and any public client that cannot store a client_secret. Also recommended for confidential clients as defense-in-depth (OAuth 2.1 mandates it for all flows).

See Also