JavaScript OAuth 2.0 (part 1) - Crypto
This is part 1 of a 3 part series that specifically goes through the code necessary to build your own OAuth 2.0 library in JavaScript. We're gonna review how to use some basic OAuth 2.0 crypto utility functions to generate randomized state and a code verifier. Then we will review how to take a code verifier and generate a code challenge which is a HMAC (SHA-256) base64 url-encoded string.
How to generate a code verifier
Implementation
const generateCodeVerifier = () => {
const PREFERRED_BYTE_LENGTH = 48;
const webCrypto = getWebCrypto();
if (webCrypto?.subtle) {
const arr = new Uint8Array(PREFERRED_BYTE_LENGTH);
webCrypto.getRandomValues(arr);
return base64UrlEncode(arr);
} else {
// Node fallback
const nodeCrypto = require("crypto");
return nodeCrypto.randomBytes(PREFERRED_BYTE_LENGTH).toString("base64url");
}
};
Unit test
// minimum 43 characters & maximum of 128 characters
describe("generateCodeVerifier", () => {
it("should generate a 32-byte base64url encoded string", () => {
const codeVerifier = cryptoLib.generateCodeVerifier();
expect(codeVerifier).toMatch(/^[A-Za-z0-9-_.~]{64}$/);
expect(codeVerifier.length >= 43).toBeTruthy();
expect(codeVerifier.length <= 128).toBeTruthy();
});
});
How to generate a code challenge
Implementation
const generateCodeChallenge = async (codeVerifier) => {
if (!codeVerifier) return null;
const webCrypto = getWebCrypto();
if (webCrypto?.subtle) {
return base64UrlEncode(
await webCrypto.subtle.digest("SHA-256", stringToBuffer(codeVerifier))
);
} else {
// Node fallback
const nodeCrypto = require("crypto");
const shaHash = nodeCrypto.createHash("sha256");
shaHash.update(stringToBuffer(codeVerifier));
return shaHash.digest("base64url");
}
};
Unit test
// Code verifier + Code Challenge directly taken from https://datatracker.ietf.org/doc/html/rfc7636
const codeVerifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
const codeChallenge = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM";
describe("generateCodeChallenge", () => {
it("should generate the matching code challenge for a given code verifier", async () => {
expect(await cryptoLib.generateCodeChallenge(codeVerifier)).toEqual(
codeChallenge
);
});
it("should not generate a code challenge if not code verifier parameter passed", async () => {
expect(await cryptoLib.generateCodeChallenge()).toBeNull();
});
});
How to generate state
Implementation
const generateRandomString = (length = 24) => {
if (length === 0) return null;
const webCrypto = getWebCrypto();
if (webCrypto?.subtle) {
const buffer = new Uint8Array(Math.ceil(length / 2));
webCrypto.getRandomValues(buffer);
return Array.from(buffer, (byte) =>
byte.toString(16).padStart(2, "0")
).join("");
} else {
// Node fallback
const nodeCrypto = require("crypto");
return nodeCrypto
.randomBytes(Math.ceil(length / 2))
.toString("hex")
.slice(0, length);
}
};
Unit test
describe("generateRandomString", () => {
it("should generate a secure random string", () => {
expect(cryptoLib.generateRandomString()).toMatch(/^[A-Za-z0-9-_]{24}$/);
});
it("should return null if length is 0", () => {
expect(cryptoLib.generateRandomString(0)).toBe(null);
});
});
Helper functions
function stringToBuffer(string) {
if (!string || string.length === 0) return null;
const buffer = new Uint8Array(string.length);
for (let i = 0; i < string.length; i++) {
buffer[i] = string.charCodeAt(i) & 0xff;
}
return buffer;
}
function base64UrlEncode(input) {
if (!input || input.length === 0) return null;
const inputType =
typeof input === "string"
? input
: String.fromCharCode(...new Uint8Array(input));
return btoa(inputType)
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
}
Hopefully some of you found that useful. Cheers! 🎉
Connect with me on Dev.to!
Top comments (0)