JavaScript OAuth 2.0 (part 1) - Crypto
Tags: javascript, es6, authentication, oauth
Published: 2023-09-05 23:38:58

---
title: JavaScript OAuth 2.0 (part 1) - Crypto
published: true
description: Create the PKCE code verifier, code challenge, and state in JavaScript from scratch.
tags: javascript, es6, authentication, oauth

author: Alexander Garcia (asg5704)
---

## 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

```javascript
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

```javascript
// 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

```javascript
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

```javascript
// 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

```javascript
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

```javascript
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

```javascript
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!