Building a SPA and hitting the /oauth/token endpoint. Getting a 400 Bad Request. The error says invalid_grant. I’m generating the code_verifier in JS and sending the SHA256 hash as code_challenge. The code exchange works locally but fails in prod. Checked the logs, the code is valid. Here is the payload I’m POSTing:
{
"grant_type": "authorization_code",
"code": "abc123",
"code_verifier": "plain_text_verifier",
"redirect_uri": "https://myapp.com/callback"
}
Is the encoding wrong?
The docs for PKCE state: “The code_verifier is a high-entropy cryptographic random string using the unreserved characters [A-Z] / [a-z] / [0-9] / ‘-’ / ‘.’ / ‘_’ / ‘~’, with a minimum length of 43 characters and a maximum length of 128 characters.” You’re likely using Base64 encoding instead of Base64URL. The standard Base64 uses + and /, which are reserved characters. The spec requires + to become - and / to become _. Also, strip the padding =.
Here’s the correct JS implementation. Don’t use btoa directly.
async function generatePKCEPair() {
const randomBytes = crypto.getRandomValues(new Uint8Array(64));
const codeVerifier = base64UrlEncode(randomBytes);
const hashBuffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(codeVerifier));
const codeChallenge = base64UrlEncode(new Uint8Array(hashBuffer));
return { codeVerifier, codeChallenge };
}
function base64UrlEncode(buffer) {
let binary = '';
const bytes = new Uint8Array(buffer);
bytes.forEach(byte => binary += String.fromCharCode(byte));
// Standard base64 to base64url conversion
const base64 = btoa(binary);
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
Check your prod logs for the exact code_verifier sent. It probably has + or / in it. The server rejects it because the hash doesn’t match the verifier. It’s not a Genesys bug. It’s a spec compliance issue. You need to ensure the verifier string matches exactly what was hashed. If you generated it locally with a library that outputs standard Base64, switch to a PKCE library or fix the encoding. The code is valid because the authorization server issued it. The token endpoint is where the verification fails. It’s a strict check. No leniency. Fix the encoding and it’ll work.