OAuth

OAuth PKCE Flow Errors โ€” Fix code_challenge and code_verifier

Updated April 2026

Reading this article? Verify your fix in real-time. Debug your OAuth error โ€” OAuthFixer โ†’

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

BugSymptomFix
Verifier not stored before redirectsessionStorage empty on callbackStore before window.location.href
Wrong hash methodChallenge does not matchAlways use SHA-256, not plain
Wrong base64 encodingChallenge character mismatchUse base64url (- and _ not + and /), no padding
Verifier regenerated on callbackNew verifier does not match original challengeRead from storage, do not regenerate
Debug your OAuth error live โ†’ OAuthFixer