When building applications that need to authenticate users via OAuth 2.0, especially using the Authorization Code flow, you might encounter the term code_verifier. If you’re like me, you might have wondered, “What is this code_verifier and why is it important?” This post will demystify code_verifier, explain its role in Proof Key for Code Exchange (PKCE), and provide practical examples to help you implement it correctly.
The Problem: Authorization Code Flow Vulnerability
The Authorization Code flow in OAuth 2.0 is widely used because it balances security and usability. However, it has a known vulnerability: if an attacker intercepts the authorization code, they can exchange it for an access token. This is particularly problematic in public clients, like single-page applications (SPAs) and mobile apps, where you can’t store a client secret securely.
Enter PKCE, which addresses this issue by adding an additional layer of security to the Authorization Code flow. Let’s dive into how it works.
What is PKCE?
Proof Key for Code Exchange (PKCE) is an extension to the Authorization Code flow designed to protect against authorization code interception attacks. It requires public clients to generate a cryptographic key pair and send a code challenge derived from the public key to the authorization server. When exchanging the authorization code for an access token, the client proves possession of the private key by sending the original code verifier.
How Does code_verifier Work?
The code_verifier is a random string generated by the client. This string is then transformed into a code_challenge using a cryptographic hash function (usually SHA-256). The code_challenge is sent to the authorization server along with the authorization request. When the client exchanges the authorization code for an access token, it sends the code_verifier.
Here’s a step-by-step breakdown:
- Generate
code_verifier: Create a random string. - Create
code_challenge: Hash thecode_verifierusing SHA-256. - Send Authorization Request: Include
code_challengeandcode_challenge_method(e.g., S256). - Receive Authorization Code: After user consent, the authorization server redirects back to the client with an authorization code.
- Exchange Authorization Code: Send the authorization code and
code_verifierto the authorization server to get the access token.
Let’s see this in action with some code.
Practical Example: Implementing PKCE in OAuth 2.0
Step 1: Generate code_verifier and code_challenge
// Generate a random string for code_verifier
function generateRandomString(length) {
let result = '';
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
const code_verifier = generateRandomString(128); // 128 characters recommended
console.log('code_verifier:', code_verifier);
// Create code_challenge using SHA-256
async function generateCodeChallenge(codeVerifier) {
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
const digest = await crypto.subtle.digest('SHA-256', data);
return btoa(String.fromCharCode(...new Uint8Array(digest)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
const code_challenge = await generateCodeChallenge(code_verifier);
console.log('code_challenge:', code_challenge);
Step 2: Send Authorization Request
const clientId = 'YOUR_CLIENT_ID';
const redirectUri = 'https://yourapp.com/callback';
const scope = 'openid profile email';
const authUrl = `https://authorization-server.com/authorize?response_type=code&client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scope)}&code_challenge=${encodeURIComponent(code_challenge)}&code_challenge_method=S256`;
window.location.href = authUrl;
Step 3: Receive Authorization Code
After the user grants permission, the authorization server redirects to your callback URL with an authorization code:
https://yourapp.com/callback?code=AUTHORIZATION_CODE_HERE
Step 4: Exchange Authorization Code for Access Token
const authorizationCode = new URLSearchParams(window.location.search).get('code');
const tokenEndpoint = 'https://authorization-server.com/token';
fetch(tokenEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: authorizationCode,
redirect_uri: redirectUri,
client_id: clientId,
code_verifier: code_verifier // Include the code_verifier here
})
})
.then(response => response.json())
.then(data => {
console.log('Access Token:', data.access_token);
})
.catch(error => console.error('Error:', error));
Common Mistakes When Implementing PKCE
- Using Weak Randomness for
code_verifier: Always generate a sufficiently long and randomcode_verifier(at least 128 characters). - Incorrect Encoding of
code_challenge: Ensure thecode_challengeis Base64 URL-encoded without padding. - Omitting
code_challenge_method: Always specify the method used to create thecode_challenge(e.g.,S256for SHA-256). - Reusing
code_verifier: Each authorization request should have a uniquecode_verifier. - Not Validating Responses: Always validate the responses from the authorization server to prevent man-in-the-middle attacks.
Security Benefits of PKCE
- Prevents Authorization Code Interception: Even if an attacker intercepts the authorization code, they cannot exchange it for an access token without the
code_verifier. - No Client Secret Required: Public clients do not need to store a client secret, reducing the risk of exposure.
- Enhanced Security for SPA and Mobile Apps: Ideal for environments where storing secrets securely is challenging.
Real-World Scenario: Debugging PKCE Issues
I’ve debugged this 100+ times, and here’s a common scenario:
Problem: You receive an error when exchanging the authorization code for an access token.
Error Message:
{
"error": "invalid_grant",
"error_description": "Invalid code verifier"
}
Solution: Double-check the following:
- Correct
code_verifier: Ensure thecode_verifierused in the token request matches the one used in the authorization request. - Proper Encoding: Verify that the
code_challengewas correctly Base64 URL-encoded without padding. - Consistent Method: Make sure the
code_challenge_methodspecified in the authorization request matches the method used to create thecode_challenge.
This saved me 3 hours last week. Always verify these details carefully.
Conclusion
Implementing PKCE in your OAuth 2.0 Authorization Code flow is crucial for securing public clients. By generating a code_verifier, creating a code_challenge, and ensuring correct usage during the authorization and token exchange processes, you can significantly enhance the security of your application.
Go ahead and implement PKCE in your projects. It’s simple, secure, works.