Awwwards Nominee
← Back to all posts3 min read

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

Visual

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

Visual

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

Visual

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! πŸŽ‰

If you enjoyed this article please feel free to connect with me on Dev.to or on LinkedIn