PKCE (Proof Key for Code Exchange)

PKCE (Proof Key for Code Exchange), pronounced "pixy," is an extension to OAuth 2.0 (RFC 7636) that prevents authorization code interception attacks. Originally designed for public clients (mobile apps, SPAs), PKCE is now recommended for all OAuth clients and required in OAuth 2.1.

The Problem PKCE Solves

In the standard OAuth authorization code flow, an attacker who intercepts the authorization code (via malicious app, URL handler hijacking, or referrer leakage) can exchange it for tokens. Public clients can't use client secrets, making them especially vulnerable.

How PKCE Works

1. Client generates random code_verifier (43-128 chars)
   code_verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"

2. Client computes code_challenge from verifier
   code_challenge = BASE64URL(SHA256(code_verifier))
   code_challenge = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"

3. Client sends authorization request WITH code_challenge
   GET /authorize?
     response_type=code&
     client_id=CLIENT_ID&
     code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
     code_challenge_method=S256&
     redirect_uri=...

4. Authorization server stores code_challenge with auth code

5. Client exchanges code WITH code_verifier
   POST /token
   {
     grant_type: authorization_code,
     code: AUTH_CODE,
     code_verifier: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
     redirect_uri: ...
   }

6. Server verifies: SHA256(code_verifier) == stored code_challenge

Why It's Secure

  • Attacker can intercept auth code AND see code_challenge (public)
  • But attacker cannot compute code_verifier from code_challenge (SHA256 is one-way)
  • Without code_verifier, auth code is useless

Implementation

// JavaScript - Generate PKCE parameters
async function generatePKCE() {
  // Generate random verifier
  const array = new Uint8Array(32);
  crypto.getRandomValues(array);
  const verifier = base64URLEncode(array);

  // Compute challenge
  const hash = await crypto.subtle.digest('SHA-256',
    new TextEncoder().encode(verifier));
  const challenge = base64URLEncode(new Uint8Array(hash));

  return { verifier, challenge };
}

function base64URLEncode(buffer) {
  return btoa(String.fromCharCode(...buffer))
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=/g, '');
}

Challenge Methods

  • S256 (recommended): code_challenge = BASE64URL(SHA256(code_verifier))
  • plain (discouraged): code_challenge = code_verifier (no transformation)

Security Considerations

  • Always use S256: Plain method provides minimal security
  • Use cryptographic random: code_verifier must be unpredictable
  • Minimum entropy: code_verifier should be at least 43 characters
  • Store verifier securely: Don't expose in localStorage for SPAs

OAuth 2.1 Changes

OAuth 2.1 (draft) makes PKCE mandatory for all clients, including confidential clients. This provides defense-in-depth even when client secrets are used.

See Also