Skip to content

Commit

Permalink
feat(jwt): add a JSON Web Token library (denoland/deno#7991)
Browse files Browse the repository at this point in the history
Co-authored-by: Tim Reichen <[email protected]>
  • Loading branch information
2 people authored and caspervonb committed Jan 31, 2021
1 parent d9c8675 commit c575905
Show file tree
Hide file tree
Showing 7 changed files with 739 additions and 0 deletions.
90 changes: 90 additions & 0 deletions jwt/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# jwt

Create and verify JSON Web Tokens.

## JSON Web Token

### create

Takes a `payload`, `key` and `header` and returns the url-safe encoded `token`.

```typescript
import { create } from "https://deno.land/std/token/mod.ts";

const payload = { foo: "bar" };
const key = "secret";

const token = await create(payload, key); // eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.4i-Q1Y0oDZunLgaorkqbYNcNfn5CgdF49UvJ7dUQ4GVTQvpsMLHABkZBWp9sghy3qVOsec6hOcu4RnbFkS30zQ
```

**Specific algorithm**

```typescript
const token = await create(payload, key, { header: { alg: "HS256" } });
```

### verify

Takes a `token`, `key` and an optional `options` object and returns the
`payload` of the `token` if the `token` is valid. Otherwise it throws an
`Error`.

```typescript
import { verify } from "https://deno.land/std/token/mod.ts";

const token =
"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.4i-Q1Y0oDZunLgaorkqbYNcNfn5CgdF49UvJ7dUQ4GVTQvpsMLHABkZBWp9sghy3qVOsec6hOcu4RnbFkS30zQ";
const key = "secret";

const payload = await verify(token, key); // { foo: "bar" }
```

**Specific algorithm**

```ts
const payload = await verify(token, key, { algorithm: "HS256" });
```

### decode

Takes a `token` to return an object with the `header`, `payload` and `signature`
properties if the `token` is valid. Otherwise it throws an `Error`.

```typescript
import { decode } from "https://deno.land/std/token/mod.ts";

const token =
"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.4i-Q1Y0oDZunLgaorkqbYNcNfn5CgdF49UvJ7dUQ4GVTQvpsMLHABkZBWp9sghy3qVOsec6hOcu4RnbFkS30zQ";

const { payload, signature, header } = await decode(token); // { header: { alg: "HS512", typ: "JWT" }, payload: { foo: "bar" }, signature: "e22f90d58d280d9ba72e06a8ae4a9b60d70d7e7e4281d178f54bc9edd510e0655342fa6c30b1c00646415a9f6c821cb7a953ac79cea139cbb84676c5912df4cd" }
```

## Expiration

The optional **exp** claim in the payload (number of seconds since January 1,
1970, 00:00:00 UTC) that identifies the expiration time on or after which the
JWT must not be accepted for processing. This module checks if the current
date/time is before the expiration date/time listed in the **exp** claim.

```typescript
const oneHour = 60 * 60;
const token = await create({ exp: Date.now() + oneHour }, "secret");
```

## Algorithms

The following signature and MAC algorithms have been implemented:

- HS256 (HMAC SHA-256)
- HS512 (HMAC SHA-512)
- none ([_Unsecured JWTs_](https://tools.ietf.org/html/rfc7519#section-6)).

## Serialization

This application uses the JWS Compact Serialization only.

## Specifications

- [JSON Web Token](https://tools.ietf.org/html/rfc7519)
- [JSON Web Signature](https://www.rfc-editor.org/rfc/rfc7515.html)
- [JSON Web Algorithms](https://www.rfc-editor.org/rfc/rfc7518.html)
17 changes: 17 additions & 0 deletions jwt/_algorithm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* JSW §1: Cryptographic algorithms and identifiers for use with this specification
* are described in the separate JSON Web Algorithms (JWA) specification:
* https://www.rfc-editor.org/rfc/rfc7518
*/
export type Algorithm = "none" | "HS256" | "HS512";
export type AlgorithmInput = Algorithm | Array<Exclude<Algorithm, "none">>;
/**
* Verify the algorithm
* @param algorithm as string or multiple algorithms in an array excluding 'none'
* @param the algorithm from the jwt header
*/
export function verify(algorithm: AlgorithmInput, jwtAlg: string): boolean {
return Array.isArray(algorithm)
? (algorithm as string[]).includes(jwtAlg)
: algorithm === jwtAlg;
}
11 changes: 11 additions & 0 deletions jwt/_algorithm_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { assertEquals } from "../testing/asserts.ts";

import { verify as verifyAlgorithm } from "./_algorithm.ts";

Deno.test("[jwt] verify algorithm", function () {
assertEquals(verifyAlgorithm("HS512", "HS512"), true);
assertEquals(verifyAlgorithm("HS512", "HS256"), false);
assertEquals(verifyAlgorithm(["HS512"], "HS512"), true);
assertEquals(verifyAlgorithm(["HS256", "HS512"], "HS512"), true);
assertEquals(verifyAlgorithm(["HS512"], "HS256"), false);
});
63 changes: 63 additions & 0 deletions jwt/_signature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { Algorithm } from "./_algorithm.ts";
import { HmacSha256 } from "../hash/sha256.ts";
import { HmacSha512 } from "../hash/sha512.ts";
import { encode as convertUint8ArrayToBase64url } from "../encoding/base64url.ts";
import { decodeString as convertHexToUint8Array } from "../encoding/hex.ts";

export function convertHexToBase64url(input: string): string {
return convertUint8ArrayToBase64url(convertHexToUint8Array(input));
}

function encrypt(
algorithm: Algorithm,
key: string,
message: string,
): string {
switch (algorithm) {
case "none":
return "";
case "HS256":
return new HmacSha256(key).update(message).toString();
case "HS512":
return new HmacSha512(key).update(message).toString();
default:
throw new RangeError(
`The algorithm of '${algorithm}' in the header is not supported.`,
);
}
}

/**
* Create a signature
* @param algorithm
* @param key
* @param input
*/
export async function create(
algorithm: Algorithm,
key: string,
input: string,
): Promise<string> {
return convertHexToBase64url(await encrypt(algorithm, key, input));
}

/**
* Verify a signature
* @param signature
* @param key
* @param alg
* @param signingInput
*/
export async function verify({
signature,
key,
algorithm,
signingInput,
}: {
signature: string;
key: string;
algorithm: Algorithm;
signingInput: string;
}): Promise<boolean> {
return signature === (await encrypt(algorithm, key, signingInput));
}
46 changes: 46 additions & 0 deletions jwt/_signature_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { assertEquals } from "../testing/asserts.ts";
import { create, decode } from "./mod.ts";

import {
convertHexToBase64url,
create as createSignature,
verify as verifySignature,
} from "./_signature.ts";

const algorithm = "HS256";
const key = "m$y-key";

Deno.test("[jwt] create signature", async function () {
// https://www.freeformatter.com/hmac-generator.html
const computedHmacInHex =
"2b9e6619fa7f2c8d8b3565c88365376b75b1b0e5d87e41218066fd1986f2c056";
assertEquals(
await createSignature(algorithm, key, "thisTextWillBeEncrypted"),
convertHexToBase64url(computedHmacInHex),
);

const anotherVerifiedSignatureInBase64Url =
"p2KneqJhji8T0PDlVxcG4DROyzTgWXbDhz_mcTVojXo";
assertEquals(
await createSignature(
algorithm,
key,
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ",
),
anotherVerifiedSignatureInBase64Url,
);
});

Deno.test("[jwt] verify signature", async function () {
const jwt = await create({}, key);
const { header, signature } = decode(jwt);

const validSignature = await verifySignature({
signature,
key,
algorithm: header.alg,
signingInput: jwt.slice(0, jwt.lastIndexOf(".")),
});

assertEquals(validSignature, true);
});
Loading

0 comments on commit c575905

Please sign in to comment.