← Return to Pantheon
🛡️

The PKCE Guardian

Shield Against the Dark Arts - Protector of Public Clients

Essential Flow

"I am the PKCE Guardian! Where applications cannot keep secrets, I provide divine protection! With my cryptographic shield, even the vilest code interceptor cannot steal your authorization! Mobile apps, single-page applications, all who cannot hide a client_secret—come under my protection!"

Mortal Translation

PKCE (Proof Key for Code Exchange, pronounced "pixy") is an extension to the Authorization Code flow designed for public clients—applications that cannot securely store a client secret. This includes single-page applications (SPAs), mobile apps, and desktop applications.

Instead of relying on a client secret, PKCE uses a dynamically generated cryptographic challenge. The app creates a random code_verifier, sends its hash (code_challenge) during authorization, and later proves it has the original verifier when exchanging the code for tokens.

The Shield of PKCE

Application

Generate code_verifier (random string) and code_challenge (SHA256 hash)

Application

Divine Commentary: The shield is forged! A random secret and its cryptographic hash—this is my divine protection!

Application

Redirect to Authorization Server with client_id, redirect_uri, code_challenge, code_challenge_method

User

Divine Commentary: I send the hash, but keep the original secret! No villain can reverse this divine encryption!

User

User authenticates and grants consent

Auth Server

Divine Commentary: The mortal proves their identity while I stand guard!

Auth Server

Returns authorization code (stores code_challenge)

Application

Divine Commentary: The authorization server remembers my challenge! The code alone is worthless without the verifier!

Application

Exchanges code + code_verifier for tokens

Auth Server

Divine Commentary: Now I reveal the original secret! The auth server verifies it matches the challenge!

Auth Server

Verifies SHA256(code_verifier) == code_challenge

Auth Server

Divine Commentary: The shield holds! Only the true application could possess the original verifier!

Auth Server

Returns access_token and refresh_token

Application

Divine Commentary: Victory! The tokens are granted, and no attacker could intercept this sacred exchange!

⚡ Interactive Flow Simulator

Experience the Authorization Code flow step-by-step with visual animations

Flow Visualization

🌐
Browser
🔐
Auth Server
🖥️
Backend
🟦 Front-channel
1

Generate PKCE Challenge

App creates random code_verifier and its SHA-256 hash (code_challenge)

🛡️ PKCE Protection: The code_verifier is kept secret in your app. Only the hash (code_challenge) is sent. This prevents authorization code interception attacks.

2

Redirect to Authorization Server

3

User Authenticates & Consents

4

Authorization Code Returned

5

Exchange Code with Verifier

6

Access Protected Resources

🛡️ Why The Guardian Was Summoned

The Problem: Mobile and single-page applications cannot securely store a client_secret. Any secret bundled in a mobile app or JavaScript can be extracted by examining the code or intercepting network traffic.

The Old Way: The Implicit Flow was used, but it passed access tokens through the browser URL—vulnerable to interception and has been deprecated.

The Divine Solution: PKCE allows these apps to use the more secure Authorization Code flow without needing a client_secret. The dynamically generated code_verifier acts as a one-time secret that an attacker cannot predict or steal.

The Divine Purpose

The Guardian speaks: "When Codeus demands a client_secret but your application cannot hide one, call upon ME! I replace the static secret with a dynamic cryptographic proof that changes with each authentication!"

In mortal terms: PKCE prevents authorization code interception attacks. Even if an attacker steals the authorization code, they cannot exchange it for tokens without the code_verifier, which only exists in your app's memory.

The Sacred Ritual

Step 1: Generate the Shield

Create a random code_verifier (43-128 characters). Generate its SHA-256 hash as the code_challenge. Store the verifier securely in memory or session storage.

Step 2: Begin Authorization

Redirect to the authorization server with the code_challenge (NOT the verifier). Include code_challenge_method=S256 to indicate SHA-256 hashing.

Step 3: User Grants Access

The user authenticates and consents. The auth server stores the code_challenge and returns an authorization code.

Step 4: Prove Possession

Exchange the authorization code for tokens, including the originalcode_verifier. The auth server hashes it and verifies it matches the stored challenge.

Ancient Wisdom (Best Practices)

  • Always use S256 method - SHA-256 hashing; plain method is only for legacy systems
  • Generate cryptographically random verifiers - Use crypto.getRandomValues(), never Math.random()
  • Store verifier in session storage - Not localStorage; clear it after token exchange
  • Implement refresh token rotation - Refresh tokens should be single-use for SPAs
  • Use a backend-for-frontend (BFF) when possible - Proxy tokens through httpOnly cookies
  • Validate state parameter - PKCE doesn't replace CSRF protection

Forbidden Arts (Common Mistakes)

  • Storing tokens in localStorage - Vulnerable to XSS attacks; use memory or httpOnly cookies
  • Using weak random generation - Math.random() is NOT cryptographically secure
  • Reusing code_verifier - Generate a fresh one for each authorization flow
  • Using plain code challenge method - S256 (SHA-256) is required for security
  • Forgetting to validate redirect_uri - Auth server must validate this matches registration

The Mortal's Guide to Implementation

PKCE Flow Implementation for SPAsjavascript
// PKCE Flow Implementation for Single-Page Applications
// This flow is perfect for apps that cannot securely store a client_secret

// Step 1: Generate code verifier and challenge
function generateCodeVerifier() {
  const array = new Uint8Array(32);
  crypto.getRandomValues(array);
  return base64URLEncode(array);
}

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

async function generateCodeChallenge(verifier) {
  const encoder = new TextEncoder();
  const data = encoder.encode(verifier);
  const hash = await crypto.subtle.digest('SHA-256', data);
  return base64URLEncode(hash);
}

// Step 2: Initiate OAuth flow with PKCE
async function initiateOAuthFlowWithPKCE() {
  // Generate and store verifier
  const codeVerifier = generateCodeVerifier();
  sessionStorage.setItem('code_verifier', codeVerifier);

  // Generate challenge
  const codeChallenge = await generateCodeChallenge(codeVerifier);

  // Build authorization URL
  const authUrl = 'https://auth-server.com/oauth/authorize';
  const params = new URLSearchParams({
    response_type: 'code',
    client_id: 'your-public-client-id', // No secret needed!
    redirect_uri: 'https://yourapp.com/callback',
    scope: 'read write',
    state: generateRandomState(),
    code_challenge: codeChallenge,
    code_challenge_method: 'S256' // SHA-256
  });

  // Redirect user
  window.location.href = `${authUrl}?${params}`;
}

// Step 3: Handle callback and exchange code for tokens
async function handleOAuthCallback(authCode, state) {
  // Verify state
  if (state !== sessionStorage.getItem('oauth_state')) {
    throw new Error('State mismatch - possible CSRF attack!');
  }

  // Retrieve stored verifier
  const codeVerifier = sessionStorage.getItem('code_verifier');
  if (!codeVerifier) {
    throw new Error('Code verifier not found');
  }

  // Exchange code for tokens
  const tokenUrl = 'https://auth-server.com/oauth/token';
  const response = await fetch(tokenUrl, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      code: authCode,
      redirect_uri: 'https://yourapp.com/callback',
      client_id: 'your-public-client-id',
      code_verifier: codeVerifier // The secret revealed!
    })
  });

  if (!response.ok) {
    throw new Error(`Token exchange failed: ${response.status}`);
  }

  const tokens = await response.json();
  // tokens = { access_token, refresh_token, expires_in, token_type }

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

  return tokens;
}

// Step 4: Secure token storage (use httpOnly cookies on your backend proxy)
// For pure SPA, use memory storage with refresh token rotation
class TokenStore {
  constructor() {
    this.accessToken = null;
    this.refreshToken = null;
  }

  setTokens(accessToken, refreshToken) {
    this.accessToken = accessToken;
    this.refreshToken = refreshToken;
    // DO NOT store in localStorage (XSS vulnerable)
  }

  async getAccessToken() {
    // Check if token is expired, refresh if needed
    if (this.isTokenExpired()) {
      await this.refreshAccessToken();
    }
    return this.accessToken;
  }

  async refreshAccessToken() {
    const response = await fetch('https://auth-server.com/oauth/token', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: new URLSearchParams({
        grant_type: 'refresh_token',
        refresh_token: this.refreshToken,
        client_id: 'your-public-client-id'
      })
    });

    const tokens = await response.json();
    this.setTokens(tokens.access_token, tokens.refresh_token);
  }
}

// Helper functions
function generateRandomState() {
  const array = new Uint8Array(16);
  crypto.getRandomValues(array);
  const state = base64URLEncode(array);
  sessionStorage.setItem('oauth_state', state);
  return state;
}

When to Consult the PKCE Guardian

Single-Page Applications (SPAs)

React, Vue, Angular apps that run entirely in the browser

Mobile Applications

iOS, Android, React Native apps that cannot secure a client_secret

Desktop Applications

Electron, native desktop apps distributed to users

Any Public Client

Applications where code can be inspected or decompiled

💡
Modern Recommendation

OAuth 2.1 recommends PKCE for ALL clients, even those with secrets, for defense in depth

The Guardian vs. Codeus: When to Choose?

AspectCodeus (Auth Code)PKCE Guardian
Client TypeConfidential (can keep secrets)Public (cannot keep secrets)
Use CasesServer-side web appsSPAs, mobile, desktop apps
Requires client_secretYesNo
Protection MethodStatic client_secretDynamic code_verifier
Code Interception RiskMitigated by client_secretMitigated by PKCE proof

Consult Also With

⚡ Codeus - For traditional web applications with backend servers

👑 Machinus - For machine-to-machine communication without user involvement

Divine Decree: RFC 7636