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\'
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 }; });
}
}
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;
}