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:
- Code Verifier: A random string generated by your app
- Code Challenge: A hash of the verifier sent with the authorization request
- 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
| Parameter | Required | Description |
|---|---|---|
code_challenge | Yes | BASE64URL(SHA256(code_verifier)) |
code_challenge_method | Yes | Must 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
| Parameter | Required | Description |
|---|---|---|
grant_type | Yes | Must be authorization_code |
code | Yes | The authorization code received |
redirect_uri | Yes | Must match the original request |
client_id | Yes | Your application's client ID |
code_verifier | Yes | The 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:
S256 (Recommended)
code_challenge = BASE64URL(SHA256(code_verifier))
This is the secure option and should always be used.
plain (Not Recommended)
code_challenge = code_verifier
Only use this if your environment doesn't support SHA-256. It provides less security.
Security Notes
- Always use S256 - The
plainmethod should only be used when SHA-256 is unavailable - Store verifier securely - Use
sessionStorage(clears on tab close) notlocalStorage - Generate new PKCE values for each authorization request
- Verifier length - Must be 43-128 characters (S-Auth generates 43-character verifiers)
- PKCE is mandatory for public clients in S-Auth