Struggling to understand why the token exchange fails with a 400 error despite correct PKCE parameters.
I am building a GraphQL gateway that wraps Genesys Cloud APIs for a single-page application. The SPA performs the initial authorization code flow with PKCE. The frontend generates the code verifier and challenge correctly. It redirects to the Genesys Cloud authorization endpoint. After user consent, it receives the code and sends it to my Node.js backend via a secure channel. The backend then calls the token endpoint to exchange the code for an access token.
The request fails with 400 Bad Request. The error response is "error": "invalid_grant" with "error_description": "Code verification failed". I am using the @genesyscloud/purecloud-platform-client-v2 SDK for the backend call. Here is the token request logic:
I have verified that the code_verifier sent by the frontend matches the one derived from the code_challenge. The redirect_uri matches exactly. The code is fresh. Is there a specific format requirement for the code_verifier in the SDK that differs from the standard RFC 7636 implementation? Or is the SDK modifying the payload before sending?
check your backend exchange logic. the 400 usually means the code_verifier you’re sending in the POST body doesn’t match the code_challenge generated on the frontend. make sure you’re using SHA-256 with URL-safe base64 encoding. no padding.
also, ensure your Node.js handler is sending the grant_type as authorization_code and not client_credentials. here’s a quick sanity check for the exchange call:
const tokenRes = await axios.post(`${baseUrl}/oauth/token`, new URLSearchParams({
grant_type: 'authorization_code',
code: req.body.code,
redirect_uri: 'http://localhost:3000/callback', // must match exactly
client_id: process.env.GC_CLIENT_ID,
code_verifier: req.body.codeVerifier // critical: must match frontend hash
}));
if that looks right, check the CloudWatch logs for the raw request payload. sometimes the URL encoding gets messy when passing between services. i’ve seen the verifier get double-encoded which kills the hash match.
yeah, that base64url padding issue is the silent killer here. i’ve seen it break SCIM syncs just as badly. if you’re generating the verifier on the frontend but hashing it on the backend for verification (or vice versa), make sure the encoding libraries match exactly.
try this quick node snippet to verify your challenge generation matches what the frontend sent. it’s easy to accidentally include = padding or use standard base64 instead of base64url.
double check that your frontend is doing the exact same transformation. also, ensure the code hasn’t expired yet. those codes are short-lived. if it’s still failing, dump the raw request body to a log and compare the hex values of the challenge.
you’re missing the URL-safe part of the base64 encoding. that = padding at the end of standard base64 is what breaks the PKCE exchange 90% of the time. genesys cloud expects base64url without padding.
if you’re hashing in node but generating the challenge in the browser, library mismatches are likely. here’s how i verify the challenge locally before sending:
stop hashing the verifier on the backend. that’s the whole point of PKCE. the frontend must generate both the verifier and the challenge. if they’re different libraries, the hash won’t match.