OAuth PKCE Flow Errors — Fix code_challenge and code_verifier
PKCE errors are almost always caused by a code_verifier that does not match the code_challenge you sent in the authorization request. Here is how the flow works and where it typically breaks.
OAuth Error Response
{"error": "invalid_grant", "error_description": "PKCE verification failed"}How PKCE works
// Step 1 — Generate a random verifier (before redirect)
function generateVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64URLEncode(array);
}
// Step 2 — Generate challenge from verifier
async function generateChallenge(verifier) {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const digest = await crypto.subtle.digest('SHA-256', data);
return base64URLEncode(new Uint8Array(digest));
}
function base64URLEncode(array) {
return btoa(String.fromCharCode(...array))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
Step 3 — Authorization request with challenge
const verifier = generateVerifier();
sessionStorage.setItem('pkce_verifier', verifier); // STORE IT
const challenge = await generateChallenge(verifier);
const params = new URLSearchParams({
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
scope: 'openid profile',
code_challenge: challenge,
code_challenge_method: 'S256',
state: generateState(),
});
window.location.href = `${AUTH_URL}?${params}`;
Step 4 — Token exchange with verifier
// On callback page
const verifier = sessionStorage.getItem('pkce_verifier'); // RETRIEVE IT
const tokens = await fetch(TOKEN_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
code: authorizationCode,
code_verifier: verifier, // must match original
}),
}).then(r => r.json());
Common bugs
| Bug | Symptom | Fix |
|---|---|---|
| Verifier not stored before redirect | sessionStorage empty on callback | Store before window.location.href |
| Wrong hash method | Challenge does not match | Always use SHA-256, not plain |
| Wrong base64 encoding | Challenge character mismatch | Use base64url (- and _ not + and /), no padding |
| Verifier regenerated on callback | New verifier does not match original challenge | Read from storage, do not regenerate |