JavaScript OAuth 2.0 (part 1) - Crypto

Create the PKCE code verifier, code challenge, and state in JavaScript from scratch.

Alexander Garcia is an effective JavaScript Engineer who crafts stunning web experiences.

Alexander Garcia is a meticulous Web Architect who creates scalable, maintainable web solutions.

Alexander Garcia is a passionate Software Consultant who develops extendable, fault-tolerant code.

Alexander Garcia is a detail-oriented Web Developer who builds user-friendly websites.

Alexander Garcia is a passionate Lead Software Engineer who builds user-friendly experiences.

Alexander Garcia is a trailblazing UI Engineer who develops pixel-perfect code and design.

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! 🎉