Skip to main content

Authorization Code + PKCE

PKCE (Proof Key for Code Exchange) is an extension to the authorization code flow that provides additional security for public clients that cannot securely store a client secret.

When to Use

  • Single Page Applications (SPAs)
  • Mobile applications (iOS, Android)
  • Desktop applications
  • CLI tools
  • Any client where the source code is visible to users

How PKCE Works

PKCE adds a "proof key" mechanism:

  1. Code Verifier: A random string generated by your app
  2. Code Challenge: A hash of the verifier sent with the authorization request
  3. Verification: When exchanging the code, send the original verifier to prove you initiated the request

This prevents authorization code interception attacks because even if an attacker steals the code, they don't have the original verifier.

Flow Diagram

┌──────────┐                              ┌──────────────┐                              ┌──────────┐
│ User │ │ Your App │ │ S-Auth │
└────┬─────┘ └──────┬───────┘ └────┬─────┘
│ │ │
│ 1. Click "Login" │ │
│ ─────────────────────────────────────────>│ │
│ │ │
│ │ Generate code_verifier │
│ │ Generate code_challenge = SHA256(verifier)
│ │ │
│ │ 2. Redirect to /authorize │
│ │ + code_challenge │
│ <─────────────────────────────────────────────────────────────────────────────────────│
│ │ │
│ 3. Login & Grant Consent │ │
│ ─────────────────────────────────────────────────────────────────────────────────────>│
│ │ │
│ 4. Redirect with code │ │
│ <─────────────────────────────────────────────────────────────────────────────────────│
│ │ │
│ │ 5. Exchange code + code_verifier │
│ │ ─────────────────────────────────────────>│
│ │ │
│ │ 6. Verify SHA256(verifier) == challenge │
│ │ Return tokens │
│ │ <─────────────────────────────────────────│
│ │ │

Step 1: Generate PKCE Values

Generate a code verifier and challenge:

// Generate random code_verifier (43-128 characters)
function generateCodeVerifier(): string {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64UrlEncode(array);
}

// Generate code_challenge from verifier
async function generateCodeChallenge(verifier: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const hash = await crypto.subtle.digest('SHA-256', data);
return base64UrlEncode(new Uint8Array(hash));
}

// Base64 URL encoding (no padding)
function base64UrlEncode(buffer: Uint8Array): string {
return btoa(String.fromCharCode(...buffer))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}

Step 2: Authorization Request with PKCE

GET https://auth.sebbyk.net/authorize?
response_type=code&
client_id=YOUR_CLIENT_ID&
redirect_uri=https://yourapp.com/callback&
scope=openid%20profile%20email&
state=RANDOM_STATE_VALUE&
code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
code_challenge_method=S256

Additional Parameters

ParameterRequiredDescription
code_challengeYesBASE64URL(SHA256(code_verifier))
code_challenge_methodYesMust be S256 (recommended) or plain

Step 3: Token Exchange with PKCE

Exchange the code, including the original verifier:

curl -X POST https://auth.sebbyk.net/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=AUTH_CODE" \
-d "redirect_uri=https://yourapp.com/callback" \
-d "client_id=YOUR_CLIENT_ID" \
-d "code_verifier=YOUR_ORIGINAL_CODE_VERIFIER"

Request Parameters

ParameterRequiredDescription
grant_typeYesMust be authorization_code
codeYesThe authorization code received
redirect_uriYesMust match the original request
client_idYesYour application's client ID
code_verifierYesThe original code verifier

Note: No client secret is required for PKCE-enabled public clients.

Example: React SPA Implementation

// pkce.ts
export async function generatePKCE() {
const verifier = generateCodeVerifier();
const challenge = await generateCodeChallenge(verifier);
return { verifier, challenge };
}

function generateCodeVerifier(): string {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64UrlEncode(array);
}

async function generateCodeChallenge(verifier: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const hash = await crypto.subtle.digest('SHA-256', data);
return base64UrlEncode(new Uint8Array(hash));
}

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

// Login.tsx
import { generatePKCE } from './pkce';

export async function startLogin() {
const { verifier, challenge } = await generatePKCE();

// Store verifier in sessionStorage (needed for token exchange)
sessionStorage.setItem('pkce_verifier', verifier);

const state = crypto.randomUUID();
sessionStorage.setItem('oauth_state', state);

const params = new URLSearchParams({
response_type: 'code',
client_id: import.meta.env.VITE_CLIENT_ID,
redirect_uri: `${window.location.origin}/callback`,
scope: 'openid profile email',
state,
code_challenge: challenge,
code_challenge_method: 'S256',
});

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

// Callback.tsx
export async function handleCallback() {
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const state = params.get('state');
const error = params.get('error');

// Verify state
if (state !== sessionStorage.getItem('oauth_state')) {
throw new Error('Invalid state');
}

if (error) {
throw new Error(`OAuth error: ${error}`);
}

// Get stored verifier
const verifier = sessionStorage.getItem('pkce_verifier');
if (!verifier) {
throw new Error('Missing PKCE verifier');
}

// Exchange code for tokens
const response = await fetch('https://auth.sebbyk.net/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code!,
redirect_uri: `${window.location.origin}/callback`,
client_id: import.meta.env.VITE_CLIENT_ID,
code_verifier: verifier,
}),
});

const tokens = await response.json();

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

return tokens;
}

Challenge Methods

S-Auth supports two challenge methods:

code_challenge = BASE64URL(SHA256(code_verifier))

This is the secure option and should always be used.

code_challenge = code_verifier

Only use this if your environment doesn't support SHA-256. It provides less security.

Security Notes

  1. Always use S256 - The plain method should only be used when SHA-256 is unavailable
  2. Store verifier securely - Use sessionStorage (clears on tab close) not localStorage
  3. Generate new PKCE values for each authorization request
  4. Verifier length - Must be 43-128 characters (S-Auth generates 43-character verifiers)
  5. PKCE is mandatory for public clients in S-Auth