12

I'm going through Okta's PCKE Flow demo to get a better understanding of how it works, and I'm having trouble reproducing the same code_challenge hash that's being generated from the code_verifier. Here's a screenshot of the demo:

Pcke Flow Demo

Using Zg6klgrnixQJ629GsawRMV8MjWvwRAr-vyvP1MHnB6X8WKZN as the code verifier, how did they produce
iF_7prUeJ6rr3jMG3LmhW3R1cZ2ecZavFqS0jtb6tzo as the code challenge?

Using this SHA256 hash calculator and Base64 Encoder, I got ODg1ZmZiYTZiNTFlMjdhYWViZGUzMzA2ZGNiOWExNWI3NDc1NzE5ZDllNzE5NmFmMTZhNGI0OGVkNmZhYjczYQ which doesn't match the expected value of iF_7prUeJ6rr3jMG3LmhW3R1cZ2ecZavFqS0jtb6tzo. What am I doing wrong to not get the expected value?

This SHA256 base 64 hash calculator from approsto gives me a value that is very close to the expected value. Using this calculator I get iF/7prUeJ6rr3jMG3LmhW3R1cZ2ecZavFqS0jtb6tzo which is one character off from the expected value (notice how there's a / instead of _).

What am I doing that's causing this discrepancy? How do I calculate the expected code_verifier value of iF_7prUeJ6rr3jMG3LmhW3R1cZ2ecZavFqS0jtb6tzo? Thanks

burnt1ce
  • 14,387
  • 33
  • 102
  • 162

4 Answers4

24

The PKCE code challenge is the Base64-URL-encoded SHA256 hash of the verifier. This means you need to take the original string, calculate the SHA256 hash of it, then Base64-URL-encode the hash. That's a lot of words, so let's walk through it.

There are two problems with what you've tried to do above:

The online SHA256 hash calculator you found outputs the hash as a hex-encoded string rather than the raw bytes. That's typically helpful, but in this case is not. So the next thing you're doing by base64 encoding is that you're base64 encoding the hex representation of the hash rather than the raw bytes. You need to use a hash function that outputs the raw bytes, and pass the raw bytes into the base64-url-encoder.

The next problem is that you need to base64-url encode, not base64 encode. Base64-URL-encoding is a minor variation of Base64 encoding, where the only difference is using the character - instead of + and _ instead of /, and trimming the = padding characters from the end. This makes it URL-safe, since otherwise the +/= characters would need to be escaped in the URL.

So, to calculate the PKCE code challenge, you need to use a SHA256 function that can give you the raw bytes, then use a modified Base64 encoding function to encode those bytes.

Here is some code in PHP that will do that:

function pkce_code_challenge($verifier) {
    $hash = hash('sha256', $verifier, true);
    return rtrim(strtr(base64_encode($hash), '+/', '-_'), '=');
}

It's also possible in plain JavaScript in a browser, but the code is slightly longer due to the complexity of the WebCrypto APIs:

function sha256(plain) { 
    // returns promise ArrayBuffer
    const encoder = new TextEncoder();
    const data = encoder.encode(plain);
    return window.crypto.subtle.digest('SHA-256', data);
}

function base64urlencode(a) {
    // Convert the ArrayBuffer to string using Uint8 array.
    // btoa takes chars from 0-255 and base64 encodes.
    // Then convert the base64 encoded to base64url encoded.
    // (replace + with -, replace / with _, trim trailing =)
    return btoa(String.fromCharCode.apply(null, new Uint8Array(a)))
        .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

async function pkce_challenge_from_verifier(v) {
    hashed = await sha256(v);
    base64encoded = base64urlencode(hashed);
    return base64encoded;
}
Machavity
  • 30,841
  • 27
  • 92
  • 100
aaronpk
  • 615
  • 5
  • 10
  • 1
    Hey! I've been watching you on Youtube. Thank you for the very detailed answer. Also thanks for the content you've been putting out as it has been very helpful (esp. the video of the advantages of PCKE over implicit flow). In case anyone else is reading, I was able to get the expected value by declaring your JS functions and calling `await pkce_challenge_from_verifier("Zg6klgrnixQJ629GsawRMV8MjWvwRAr-vyvP1MHnB6X8WKZN")` – burnt1ce Jan 26 '20 at 14:42
  • So we don't need to pad the code challenge with the `=` character? – Peter Schorn Oct 02 '20 at 23:56
  • @PeterSchorn - Looks like the `=` padding isn't required. According to the [spec](https://datatracker.ietf.org/doc/html/rfc4648): *"The pad character "=" is typically percent-encoded when used in an URI [9], but if the data length is known implicitly, this can be avoided by skipping the padding; see section 3.2."* – Poc275 Dec 30 '21 at 10:01
  • Hey, Are there any online urls that do this "input (code verifier)->sha256 (binary value)->to base64urlencode" conversion so i can follow along without writing any code? I'm going from https://string-o-matic.com/sha256 to sha256 the input (tried all the options i thought there) then Base64 that result here: https://www.base64encode.org But not getting the code_challenge result. – armyofda12mnkeys Aug 26 '23 at 01:42
  • Check out https://example-app.com/pkce it's a tool I made to help debug these kinds of things – aaronpk Aug 27 '23 at 02:22
4

Based on Aaron's example and hacking the pkce-challenge node package, here's what I use:

class PkceChallenge {
    random(length, mask) {
        let result = "";
        let randomIndices = new Int8Array(length);
        window.crypto.getRandomValues(randomIndices);
        const byteLength = 256
        const maskLength = Math.min(mask.length, byteLength);
        const scalingFactor = byteLength / maskLength;

        for (var i = 0; i < length; i++) {
            result += mask[Math.floor(Math.abs(randomIndices[i]) / scalingFactor)];
        }
        return result;
    }

    base64UrlEncode(array) {
        return btoa(String.fromCharCode.apply(null, new Uint8Array(array)))
            .replace(/\+/g, '-')
            .replace(/\//g, '_')
            .replace(/=+$/, '');
    }

    generateVerifier(length) {
        const mask = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~";
        return this.random(length, mask);
    }

    generateChallenge(length = 43) {
        this.verifier = this.generateVerifier(length);

        const encoder = new TextEncoder();
        const data = encoder.encode(this.verifier);
        return window.crypto.subtle.digest('SHA-256', data).then(array => { return { code_challenge: this.base64UrlEncode(array), code_verifier: this.verifier }; });
    }
}
Liam
  • 5,033
  • 2
  • 30
  • 39
1
echo -n "qwe" | sha256sum -b | xxd -p -r | base64 | tr '/+' '_-' | tr -d '='

where is qwe is code_verifier

Stefano Sansone
  • 2,377
  • 7
  • 20
  • 39
1

I just had the same issue and wanted to create the code_challenge within Postman as a pre-request script and came up with the following. Might help you if you try to achieve the same. My code_verifier is set in an environment and the challenge value will be saved to the environment as well:

pm.environment.set(
    "code_challenge",
    CryptoJS.SHA256(pm.environment.get("code_verifier"))
        .toString(CryptoJS.enc.Base64)
        .replace("+", "-")
        .replace("/", "_")
        .replace(/=+$/, "")
)
dArignac
  • 1,205
  • 4
  • 11
  • 25
  • 1
    This answer helped me, but I ran into an issue when the encoded value contained more than one plus sign and had to modify it. `var code_challenge = CryptoJS.SHA256(code_verifier).toString(CryptoJS.enc.Base64).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");` – gthazmatt Jun 02 '22 at 17:26