I am trying to implement a simple Okta PKCE authentication. I am getting redirected to my callback URL with this error:
error=invalid_request&error_description=PKCE+code+challenge+contains+illegal+characters.
There is an article describing it, but it is not really helpful. The problem seems to be that my code_challenge
contains the characters +
and =
, which is totally normal for base64. The okta docs say:
The
code_challenge
is a Base64-encoded SHA256 hash of thecode_verifier
This is how I generate the hash, for testing I just use constant string:
import * as node_crypto from 'node:crypto';
class OktaAccessManager {
constructor() {
// todo: remember and periodically purge
this.challenges = new Map();
}
createChallenge() {
const randomString = "ddd";
const hash = node_crypto.createHash('sha256')
.update(randomString)
.digest('base64');
const challenge = {
hash: hash,
method: 'sha256',
secretString: randomString
};
return challenge;
}
Then I just generate simple redirect in my Express server:
const redirectUrl = `https://${SERVER_HOSTNAME}/auth/okta-callback`;
const challenge = oktaManager.createChallenge();
const params = new URLSearchParams({
client_id: "XXXXXXXXXXXXXXXXX",
state: "state-AAAAAAA-AAAA-AAAA-25cf-386e3d8967e9",
redirect_uri: redirectUrl,
response_type: "code",
scope: "email openid",
code_challenge: challenge.hash,
code_challenge_method: "S256",
});
const fullURL = new URL(`https://${OKTA_SERVER}/oauth2/v1/authorize`);
fullURL.search = params.toString();
res.header("Cache-Control", "no-store");
res.header("Pragma", "no-cache");
res.header("X-Content-Type-Options", "nosniff");
res.header("X-Frame-Options", "DENY");
res.header("X-XSS-Protection", "1; mode=block");
res.redirect(301, fullURL.toString());
When I do this, I get redirected to my redirect_uri
with the GET params describing the error.
So what exactly do I do with the base64 so that okta accepts it?
The specs say this:
S256
code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
So that's a different encoding than base64
, called base64url
in Node and supported directly by the digest
method. All I had to do is change the argument there:
const hash = node_crypto.createHash('sha256')
.update(randomString)
.digest('base64url');