PKCE Authorization Code Flow 400 Bad Request on Token Exchange

I’m implementing the Authorization Code flow with PKCE for a single-page application that instruments Genesys Cloud API calls via New Relic. The initial authorization request to https://api.mypurecloud.com/oauth/authorize works fine, and I receive the code and state parameters in the redirect callback. However, when I attempt to exchange the code for an access token, the server returns a 400 Bad Request.

Here’s the POST request body I’m sending to https://api.mypurecloud.com/oauth/token:

{
 "grant_type": "authorization_code",
 "code": "AUTH_CODE_FROM_CALLBACK",
 "redirect_uri": "https://myapp.local/callback",
 "client_id": "MY_CLIENT_ID",
 "code_verifier": "MY_CODE_VERIFIER"
}

The response is:

{
 "error": "invalid_grant",
 "error_description": "The authorization code has expired or was already used."
}

I’ve verified the following:

  • The code_verifier matches the code_challenge sent in the initial request (S256 method).
  • The redirect_uri is identical in both requests.
  • The request is made within 10 seconds of receiving the code.
  • The client_id is registered for the SPA application type.

Is there a specific timing constraint I’m missing, or could there be an issue with how the code_challenge is being generated? I’m using a standard SHA-256 hash base64url encoded.

Check your code_verifier handling. The doc says:

“The client MUST use the same code_verifier value that was used in the authorization request.”

Common mistake is generating the verifier twice. You generate it for /authorize, then generate a new one for /token. That fails PKCE validation.

Keep the original verifier in memory or local storage. Hash it with SHA-256, then base64url encode it. That’s the code_challenge you sent earlier. Now send the raw verifier in the token exchange.

Here’s the correct curl structure:

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

If you’re using a library, ensure it’s not re-generating the challenge on the token step. The server compares the hash of the sent code_verifier against the stored code_challenge. If they don’t match, you get a 400.

Double check the base64url encoding too. Standard base64 uses + and /. OAuth requires - and _. Missing that replacement breaks the hash comparison.