DEV Community

Alexander Garcia
Alexander Garcia

Posted on

JavaScript OAuth 2.0 (part 1) - Crypto

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");
  }
};
Enter fullscreen mode Exit fullscreen mode

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();
  });
});
Enter fullscreen mode Exit fullscreen mode

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");
  }
};
Enter fullscreen mode Exit fullscreen mode

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();
  });
});
Enter fullscreen mode Exit fullscreen mode

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);
  }
};
Enter fullscreen mode Exit fullscreen mode

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);
  });
});
Enter fullscreen mode Exit fullscreen mode

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(/=+$/, "");
}
Enter fullscreen mode Exit fullscreen mode

Hopefully some of you found that useful. Cheers! 🎉

Connect with me on Dev.to!

Top comments (0)