javascriptnode.jsexpressoktaokta-api

Okta "PKCE code challenge contains illegal characters" - because base64 includes "+" and "="


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 the code_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?


Solution

  • 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');