PKCE code_verifier mismatch in SPA OAuth flow

I’m building a custom single-page application that needs to authenticate users via the Authorization Code flow with PKCE. The goal is to avoid storing secrets in the browser. I’ve generated the code_verifier and code_challenge using SHA-256 as per the spec. The initial request to /oauth/token works fine, but the exchange fails. The API returns a 400 Bad Request with invalid_grant. The error message says the code verifier does not match. I’ve double checked the base64url encoding. It seems correct. Here’s the snippet for generating the challenge.

async function generatePKCE() {
 const verifier = crypto.getRandomValues(new Uint8Array(32));
 const encoded = btoa(String.fromCharCode(...verifier)).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
 const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(encoded));
 const challenge = btoa(String.fromCharCode(...new Uint8Array(hash))).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
 return { verifier: encoded, challenge };
}

I pass code_challenge_method=S256 in the auth request. Then I send the code and code_verifier to /oauth/token. The token endpoint rejects it. I’ve tried raw base64 too. No luck. The docs mention specific encoding rules but they’re vague on edge cases. Is there a library I should use instead of manual encoding? Or am I missing a step in the verifier generation? The error doesn’t give much detail. Just invalid_grant. I need to get this working before the sprint ends. Any ideas why the verifier mismatch happens?

The issue is almost certainly how you’re encoding the code challenge. The docs say: “The code_challenge parameter must be the base64url-encoded SHA-256 hash of the code_verifier.” Standard Base64 uses + and /. Base64Url uses - and _ and strips padding. If you use Convert.ToBase64String, you get the wrong characters. Genesys rejects it immediately.

Here is the correct C# implementation using WebEncoders:

using Microsoft.AspNetCore.WebUtilities;

string codeVerifier = GenerateRandomString(64);
byte[] sha256 = SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(codeVerifier));
string codeChallenge = WebEncoders.Base64UrlEncode(sha256);

Make sure you send the exact same codeVerifier string in the token exchange request. Don’t re-generate it. I’ve burned hours on this in Azure Functions. The timing is fine, but the encoding is strict. Also check for trailing newlines if you’re generating the verifier manually. It happens.