Authorization Code Flow with PKCE Explained


If OAuth terms feel confusing, you are not alone.

This post explains Authorization Code Flow with PKCE in plain language with simple examples.

What Problem Does PKCE Solve?

In OAuth Authorization Code Flow, the app gets an authorization code first, then exchanges it for tokens.

The risk: if an attacker steals that authorization code, they may try to exchange it and get tokens.

PKCE (Proof Key for Code Exchange) adds a one-time secret so the stolen code alone is not enough.

In short:

  1. App creates a random secret called code_verifier
  2. App derives code_challenge from that verifier
  3. App sends only code_challenge during login
  4. Later, app must send original code_verifier to get tokens
  5. Auth server verifies both match

If they do not match, token exchange fails.

Key Terms (Quick)

  • Authorization Endpoint: where user signs in and grants consent
  • Token Endpoint: where app exchanges code for tokens
  • code_verifier: high-entropy random string created by client
  • code_challenge: transformed version of code_verifier (usually SHA-256 + Base64URL)
  • S256: recommended challenge method

Flow in 7 Steps

  1. Client app generates code_verifier
  2. Client app creates code_challenge = BASE64URL(SHA256(code_verifier))
  3. Client redirects browser to authorization endpoint with code_challenge and code_challenge_method=S256
  4. User logs in and approves
  5. Authorization server redirects back with authorization_code
  6. Client sends authorization_code + original code_verifier to token endpoint
  7. Server validates verifier/challenge match and returns tokens

Visual Sequence

sequenceDiagram participant C as Client App participant B as Browser participant A as Authorization Server Note over C: Generate code_verifier Note over C: Create code_challenge (S256) C->>B: Redirect to /authorize with code_challenge B->>A: GET /authorize A-->>B: Login and consent screen B->>A: User authenticates and approves A-->>B: Redirect with authorization_code B-->>C: Callback with code C->>A: POST /token with code + code_verifier A->>A: Validate verifier matches original challenge A-->>C: Return access token (and optional refresh/id token)
Client App                         Authorization Server
----------                         --------------------
Generate code_verifier
Create code_challenge

Browser -> /authorize?code_challenge=...&method=S256
                                   User login + consent
<-- Redirect with authorization_code

POST /token
  grant_type=authorization_code
  code=...
  code_verifier=...              Validate verifier against challenge
<-- access_token (+ optional refresh_token, id_token)

Example 1: Build PKCE Values (JavaScript)

This example shows the core PKCE generation logic.

async function generatePkcePair() {
  const randomBytes = new Uint8Array(32);
  crypto.getRandomValues(randomBytes);

  const codeVerifier = base64UrlEncode(randomBytes);
  const data = new TextEncoder().encode(codeVerifier);
  const digest = await crypto.subtle.digest("SHA-256", data);
  const codeChallenge = base64UrlEncode(new Uint8Array(digest));

  return { codeVerifier, codeChallenge };
}

function base64UrlEncode(bytes) {
  let binary = "";
  bytes.forEach((b) => (binary += String.fromCharCode(b)));

  return btoa(binary)
    .replace(/\+/g, "-")
    .replace(/\//g, "_")
    .replace(/=+$/g, "");
}

Example 2: Build PKCE Values (C#)

This C# example generates a secure code_verifier and code_challenge using S256.

using System;
using System.Security.Cryptography;
using System.Text;

public static class PkceGenerator
{
  public static (string CodeVerifier, string CodeChallenge) Generate()
  {
    var randomBytes = new byte[32];
    RandomNumberGenerator.Fill(randomBytes);

    var codeVerifier = Base64UrlEncode(randomBytes);
    var challengeBytes = SHA256.HashData(Encoding.ASCII.GetBytes(codeVerifier));
    var codeChallenge = Base64UrlEncode(challengeBytes);

    return (codeVerifier, codeChallenge);
  }

  private static string Base64UrlEncode(byte[] bytes)
  {
    return Convert.ToBase64String(bytes)
      .TrimEnd('=')
      .Replace('+', '-')
      .Replace('/', '_');
  }
}

Example 3: Authorization Request

GET https://login.example.com/oauth2/v2.0/authorize?
  response_type=code
  &client_id=YOUR_CLIENT_ID
  &redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback
  &scope=openid%20profile%20api.read
  &state=abc123
  &code_challenge=YOUR_CODE_CHALLENGE
  &code_challenge_method=S256

Notes:

  • state helps prevent CSRF
  • Use HTTPS redirect URIs
  • Keep scopes minimal

Example 4: Token Exchange Request

POST https://login.example.com/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&client_id=YOUR_CLIENT_ID
&code=AUTHORIZATION_CODE
&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback
&code_verifier=ORIGINAL_CODE_VERIFIER

If code_verifier does not correspond to the original challenge, token issuance is denied.

Where PKCE Is Required (and Why)

PKCE is strongly recommended for all OAuth clients and essential for public clients:

  1. Single-page apps (SPA)
  2. Mobile apps
  3. Desktop apps

These clients cannot safely store a client secret, so PKCE is the security anchor for code exchange.

Common Mistakes

  1. Using plain instead of S256 Use S256 unless there is a hard compatibility reason.

  2. Reusing the same verifier Create a new code_verifier for every authorization request.

  3. Missing state validation Always validate state on callback.

  4. Weak verifier randomness Use cryptographically secure random bytes, not Math.random().

  5. Forgetting redirect URI consistency The redirect_uri at token exchange must match the one used in authorization.

Final Thoughts

Authorization Code Flow with PKCE is the modern default for secure OAuth login in browser and mobile applications.

If you remember just one thing, remember this:

PKCE binds the authorization code to the original client request, making stolen codes much less useful.


References