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_verifierlength (43-128 chars) - Check for
=padding removal incode_challenge - Ensure
application/x-www-form-urlencodedheader - Validate unreserved characters in verifier