pkce-challenge
Version:
Generate or verify a Proof Key for Code Exchange (PKCE) challenge pair
94 lines (93 loc) • 3.85 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateChallenge = generateChallenge;
exports.default = pkceChallenge;
exports.verifyChallenge = verifyChallenge;
let crypto;
crypto =
globalThis.crypto?.webcrypto ?? // Node.js [18-16] REPL
globalThis.crypto ?? // Node.js >18
import("node:crypto").then((m) => m.webcrypto); // Node.js <18 Non-REPL
/**
* Creates an array of length `size` of random bytes
* @param size
* @returns Array of random ints (0 to 255)
*/
async function getRandomValues(size) {
return (await crypto).getRandomValues(new Uint8Array(size));
}
/**
* Base64url-encode octets per RFC 7636 Appendix A
* @param buffer The octets to encode
* @returns Base64url encoded string without padding
*/
function base64urlEncode(buffer) {
// btoa is deprecated in Node.js but is used here for web browser compatibility
// (which has no good replacement yet, see also https://github.com/whatwg/html/issues/6811)
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
return btoa(String.fromCharCode(...bytes))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
}
/** Generate a PKCE challenge verifier
* @param length Length of the verifier (43-128 characters)
* @returns A random verifier `length` characters long
*/
async function generateVerifier(length) {
// Per RFC 7636 Section 4.1: recommended approach is to generate random octets
// and base64url-encode them. Base64url encoding produces 4 characters per 3 octets.
// Calculate octets needed: we need ceil(length * 3 / 4) octets to get at least
// `length` characters after encoding
const octetSize = Math.ceil((length * 3) / 4);
const octets = await getRandomValues(octetSize);
const encoded = base64urlEncode(octets);
// Trim to exact length requested (base64url encoding may produce slightly more)
return encoded.slice(0, length);
}
/** Generate a PKCE code challenge from a code verifier
* @param code_verifier base64url encoded code verifier
* @param method The challenge method to use (defaults to "S256")
* @returns The base64 url encoded code challenge (or plain verifier if method is "plain")
*/
async function generateChallenge(code_verifier, method = "S256") {
if (method === "plain") {
return code_verifier;
}
if (method !== "S256") {
throw new Error(`Unsupported PKCE challenge method: ${method}`);
}
const buffer = await (await crypto).subtle.digest("SHA-256", new TextEncoder().encode(code_verifier));
return base64urlEncode(buffer);
}
/** Generate a PKCE challenge pair
* @param length Length of the verifer (between 43-128). Defaults to 43.
* @param method The challenge method to use. Defaults to "S256".
* @returns PKCE challenge pair
*/
async function pkceChallenge(length, method) {
if (!length)
length = 43;
if (!method)
method = "S256";
if (length < 43 || length > 128) {
throw new Error(`Expected a length between 43 and 128. Received ${length}.`);
}
const verifier = await generateVerifier(length);
const challenge = await generateChallenge(verifier, method);
return {
code_verifier: verifier,
code_challenge: challenge,
code_challenge_method: method,
};
}
/** Verify that a code_verifier produces the expected code challenge
* @param code_verifier
* @param expectedChallenge The code challenge to verify
* @param method The challenge method used (defaults to "S256")
* @returns True if challenges are equal. False otherwise.
*/
async function verifyChallenge(code_verifier, expectedChallenge, method = "S256") {
const actualChallenge = await generateChallenge(code_verifier, method);
return actualChallenge === expectedChallenge;
}