If you are building a mobile app or a Single Page Application (SPA) using standard OAuth 2.0, you are likely vulnerable to an Authorization Code Interception Attack. A malicious app installed on a user's device can "listen" to your redirect URI and steal the login code, hijacking the user's session. Here is how we fix it using PKCE.
PKCE (Proof Key for Code Exchange) is a security extension for OAuth 2.0 (RFC 7636) that prevents code interception attacks. It works by creating a dynamic, one-time cryptographic secret (the code_verifier) for every authorization request, ensuring that the app exchanging the code for a token is the same one that initiated the login.
The "Luggage Claim" Analogy
Concept: Imagine checking your luggage at an airport.
- Standard OAuth: You get a claim ticket. Anyone who finds that ticket can pick up your bag.
- OAuth with PKCE: You get a claim ticket, but you also secretly memorize a password (The
Code Verifier). When you pick up the bag, you must present the ticket AND the password. Even if a thief steals your ticket (the Authorization Code), they cannot get your bag (the Access Token) because they don't know the password.
In technical terms, PKCE cryptographically binds the Authorization Request (Front Channel) to the Token Request (Back Channel). This makes the intercepted code useless without the original verifier.
Production-Ready Implementation (TypeScript)
The most common failure point in PKCE implementation is the Base64URL encoding. Standard Base64 includes symbols like + and / which break in URLs. You must replace them according to the spec.
Below is a dependency-free implementation using the modern Web Crypto API (available in all modern browsers and Node.js 19+).
/**
* PKCE Generator for OAuth 2.0 / OIDC
* Target: Modern Browsers (Edge, Chrome, Safari, Firefox)
*/
// 1. Generate a random high-entropy string (The Code Verifier)
// RFC 7636 recommends a length between 43 and 128 characters.
function generateCodeVerifier(): string {
const array = new Uint32Array(56 / 2); // 56 bytes -> ~74 chars base64
crypto.getRandomValues(array);
return Array.from(array, dec => ('0' + dec.toString(16)).substr(-2)).join('');
}
// 2. Base64URL Encode (Crucial Step)
// Replaces '+' with '-', '/' with '_', and removes '=' padding.
function base64UrlEncode(str: ArrayBuffer): string {
const bytes = new Uint8Array(str);
const len = bytes.byteLength;
let binary = '';
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
// 3. Generate Code Challenge (SHA-256)
async function generateCodeChallenge(verifier: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const digest = await crypto.subtle.digest('SHA-256', data);
return base64UrlEncode(digest);
}
// usage example
(async () => {
const verifier = generateCodeVerifier();
const challenge = await generateCodeChallenge(verifier);
console.log("Save this to sessionStorage:", verifier);
console.log("Send this in Auth URL:", challenge);
})();
Watch out for: Never use plain as the code_challenge_method if your server supports S256. The plain method sends the verifier as-is, which defeats the purpose of protection if the initial request is intercepted.
The Secure Flow Checklist
- Client: Generates
code_verifierandcode_challenge. - Client: Sends
code_challengeand `method=S256` to the/authorizeendpoint. - Client: Saves
code_verifierinsessionStorage(do not expose to URL). - Auth Server: Returns `authorization_code`.
- Client: Sends `authorization_code` AND the original `code_verifier` to the
/tokenendpoint. - Auth Server: Hashes the received `code_verifier` and checks if it matches the stored `code_challenge`.
Frequently Asked Questions
Q. Is PKCE required for backend (confidential) clients?
A. Historically, no, but OAuth 2.1 makes PKCE mandatory for ALL clients, including confidential ones using Client Secrets. It adds a layer of defense against Code Injection attacks, which can happen even on backends.
Q. Can I use the Implicit Flow instead?
A. No. The Implicit Flow is deprecated in OAuth 2.1. It returns tokens directly in the URL (hash fragment), which is a massive security risk (history leakage, referrer leakage). You should migrate to "Authorization Code Flow with PKCE".
Q. What if my Auth Provider doesn't support S256?
A. You should seriously consider switching providers. S256 (SHA-256) is the industry standard. If you must use plain, ensure your redirect URIs are strictly matched and you are using HTTPS, but know that you are less protected against interception.
Post a Comment