Pkce code challenge failing with 400 bad request on genesys cloud oauth

Does anyone know why my spa is getting a 400 bad request when exchanging the auth code? I am trying to implement the authorization code flow with pkce for a single page app. the code_verifier matches the base64url encoded sha256 of the challenge but the response says invalid grant. here is the curl snippet i am using to debug: curl -X POST https://api.mypurecloud.com/oauth/token -d 'grant_type=authorization_code&code=...&redirect_uri=...&code_verifier=...' any ideas?

TL;DR: Ensure form-urlencoded format.

This looks like a missing Content-Type header issue. The OAuth endpoint strictly requires application/x-www-form-urlencoded. Your curl command lacks the header, causing the server to misparse the body.

curl -X POST https://api.mypurecloud.com/oauth/token \
 -H "Content-Type: application/x-www-form-urlencoded" \
 -d 'grant_type=authorization_code&code=...&redirect_uri=...&code_verifier=...'

This is caused by missing Content-Type headers or malformed code_verifier encoding. I verified the application/x-www-form-urlencoded requirement. Ensure your SHA-256 hash is Base64 URL-safe encoded.

curl -X POST https://api.mypurecloud.com/oauth/token \
 -H "Content-Type: application/x-www-form-urlencoded" \
 -d 'grant_type=authorization_code&code=...&code_verifier=...'

This is caused by improper Base64 URL-safe encoding of the SHA-256 hash.

Cause: The code_verifier must use URL-safe Base64 (no padding, - instead of +, _ instead of /). Standard Base64 fails the PKCE validation.

Solution: Ensure your encoding strips padding and replaces characters correctly.

// Correct PKCE encoding logic
const crypto = require('crypto');
const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');

It depends, but typically the issue lies in the encoding step between generating the verifier and creating the challenge. The suggestion above regarding Content-Type is correct, but often overlooked is the strict requirement for URL-safe Base64 encoding without padding. Standard Base64 includes = padding and uses + and /, which breaks the SHA-256 hash comparison on the server side.

Here is the correct Node.js implementation for generating the challenge from a verifier:

const crypto = require('crypto');
const generateCodeChallenge = (verifier) => {
 const hash = crypto.createHash('sha256').update(verifier).digest('base64');
 // Convert to URL-safe base64: replace + with -, / with _, and remove =
 return hash.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
};

If your code_verifier is not exactly 43-128 characters of unreserved characters, the OAuth endpoint will reject it with a 400 error regardless of headers.

  • Verify code_verifier length (43-128 chars)
  • Check for = padding removal in code_challenge
  • Ensure application/x-www-form-urlencoded header
  • Validate unreserved characters in verifier