mirror of
https://github.com/denoland/deno.git
synced 2024-12-25 08:39:09 -05:00
feat(std/jwt): add a JSON Web Token library (#7991)
Co-authored-by: Tim Reichen <timreichen@users.noreply.github.com>
This commit is contained in:
parent
992c2a436e
commit
034ab48086
7 changed files with 739 additions and 0 deletions
90
std/jwt/README.md
Normal file
90
std/jwt/README.md
Normal file
|
@ -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
std/jwt/_algorithm.ts
Normal file
17
std/jwt/_algorithm.ts
Normal file
|
@ -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
std/jwt/_algorithm_test.ts
Normal file
11
std/jwt/_algorithm_test.ts
Normal file
|
@ -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
std/jwt/_signature.ts
Normal file
63
std/jwt/_signature.ts
Normal file
|
@ -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
std/jwt/_signature_test.ts
Normal file
46
std/jwt/_signature_test.ts
Normal file
|
@ -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);
|
||||||
|
});
|
208
std/jwt/mod.ts
Normal file
208
std/jwt/mod.ts
Normal file
|
@ -0,0 +1,208 @@
|
||||||
|
import type { Algorithm, AlgorithmInput } from "./_algorithm.ts";
|
||||||
|
import * as base64url from "../encoding/base64url.ts";
|
||||||
|
import { encodeToString as convertUint8ArrayToHex } from "../encoding/hex.ts";
|
||||||
|
import {
|
||||||
|
create as createSignature,
|
||||||
|
verify as verifySignature,
|
||||||
|
} from "./_signature.ts";
|
||||||
|
import { verify as verifyAlgorithm } from "./_algorithm.ts";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* JWT §4.1: The following Claim Names are registered in the IANA
|
||||||
|
* "JSON Web Token Claims" registry established by Section 10.1. None of the
|
||||||
|
* claims defined below are intended to be mandatory to use or implement in all
|
||||||
|
* cases, but rather they provide a starting point for a set of useful,
|
||||||
|
* interoperable claims.
|
||||||
|
* Applications using JWTs should define which specific claims they use and when
|
||||||
|
* they are required or optional.
|
||||||
|
*/
|
||||||
|
export interface PayloadObject {
|
||||||
|
iss?: string;
|
||||||
|
sub?: string;
|
||||||
|
aud?: string[] | string;
|
||||||
|
exp?: number;
|
||||||
|
nbf?: number;
|
||||||
|
iat?: number;
|
||||||
|
jti?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Payload = PayloadObject | string;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* JWS §4.1.1: The "alg" value is a case-sensitive ASCII string containing a
|
||||||
|
* StringOrURI value. This Header Parameter MUST be present and MUST be
|
||||||
|
* understood and processed by implementations.
|
||||||
|
*/
|
||||||
|
export interface Header {
|
||||||
|
alg: Algorithm;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* JWT §4.1.4: Implementers MAY provide for some small leeway to account for
|
||||||
|
* clock skew.
|
||||||
|
*/
|
||||||
|
function isExpired(exp: number, leeway = 0): boolean {
|
||||||
|
return exp + leeway < Date.now() / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryToParsePayload(input: string): unknown {
|
||||||
|
try {
|
||||||
|
return JSON.parse(input);
|
||||||
|
} catch {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes a token into an { header, payload, signature } object.
|
||||||
|
* @param token
|
||||||
|
*/
|
||||||
|
export function decode(
|
||||||
|
token: string,
|
||||||
|
): {
|
||||||
|
header: Header;
|
||||||
|
payload: unknown;
|
||||||
|
signature: string;
|
||||||
|
} {
|
||||||
|
const parsedArray = token
|
||||||
|
.split(".")
|
||||||
|
.map(base64url.decode)
|
||||||
|
.map((uint8Array, index) => {
|
||||||
|
switch (index) {
|
||||||
|
case 0:
|
||||||
|
try {
|
||||||
|
return JSON.parse(decoder.decode(uint8Array));
|
||||||
|
} catch {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 1:
|
||||||
|
return tryToParsePayload(decoder.decode(uint8Array));
|
||||||
|
case 2:
|
||||||
|
return convertUint8ArrayToHex(uint8Array);
|
||||||
|
}
|
||||||
|
throw TypeError("The serialization is invalid.");
|
||||||
|
});
|
||||||
|
|
||||||
|
const [header, payload, signature] = parsedArray;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
(typeof signature === "string" &&
|
||||||
|
typeof header?.alg === "string") && payload?.exp !== undefined
|
||||||
|
? typeof payload.exp === "number"
|
||||||
|
: true
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error(`The token is invalid.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof payload?.exp === "number" &&
|
||||||
|
isExpired(payload.exp)
|
||||||
|
) {
|
||||||
|
throw RangeError("The token is expired.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
header,
|
||||||
|
payload,
|
||||||
|
signature,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type VerifyOptions = {
|
||||||
|
algorithm?: AlgorithmInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies a token.
|
||||||
|
* @param token
|
||||||
|
* @param key
|
||||||
|
* @param object with property 'algorithm'
|
||||||
|
*/
|
||||||
|
export async function verify(
|
||||||
|
token: string,
|
||||||
|
key: string,
|
||||||
|
{ algorithm = "HS512" }: VerifyOptions = {},
|
||||||
|
): Promise<unknown> {
|
||||||
|
const { header, payload, signature } = decode(token);
|
||||||
|
|
||||||
|
if (!verifyAlgorithm(algorithm, header.alg)) {
|
||||||
|
throw new Error(
|
||||||
|
`The token's algorithm does not match the specified algorithm '${algorithm}'.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* JWS §4.1.11: The "crit" (critical) Header Parameter indicates that
|
||||||
|
* extensions to this specification and/or [JWA] are being used that MUST be
|
||||||
|
* understood and processed.
|
||||||
|
*/
|
||||||
|
if ("crit" in header) {
|
||||||
|
throw new Error(
|
||||||
|
"The 'crit' header parameter is currently not supported by this module.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(await verifySignature({
|
||||||
|
signature,
|
||||||
|
key,
|
||||||
|
algorithm: header.alg,
|
||||||
|
signingInput: token.slice(0, token.lastIndexOf(".")),
|
||||||
|
}))
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"The token's signature does not match the verification signature.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* JSW §7.1: The JWS Compact Serialization represents digitally signed or MACed
|
||||||
|
* content as a compact, URL-safe string. This string is:
|
||||||
|
* BASE64URL(UTF8(JWS Protected Header)) || '.' ||
|
||||||
|
* BASE64URL(JWS Payload) || '.' ||
|
||||||
|
* BASE64URL(JWS Signature)
|
||||||
|
*/
|
||||||
|
function createSigningInput(header: Header, payload: Payload): string {
|
||||||
|
return `${
|
||||||
|
base64url.encode(
|
||||||
|
encoder.encode(JSON.stringify(header)),
|
||||||
|
)
|
||||||
|
}.${
|
||||||
|
base64url.encode(
|
||||||
|
encoder.encode(
|
||||||
|
typeof payload === "string" ? payload : JSON.stringify(payload),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a token.
|
||||||
|
* @param payload
|
||||||
|
* @param key
|
||||||
|
* @param object with property 'header'
|
||||||
|
*/
|
||||||
|
export async function create(
|
||||||
|
payload: Payload,
|
||||||
|
key: string,
|
||||||
|
{
|
||||||
|
header = { alg: "HS512", typ: "JWT" },
|
||||||
|
}: {
|
||||||
|
header?: Header;
|
||||||
|
} = {},
|
||||||
|
): Promise<string> {
|
||||||
|
const signingInput = createSigningInput(header, payload);
|
||||||
|
const signature = await createSignature(header.alg, key, signingInput);
|
||||||
|
|
||||||
|
return `${signingInput}.${signature}`;
|
||||||
|
}
|
304
std/jwt/test.ts
Normal file
304
std/jwt/test.ts
Normal file
|
@ -0,0 +1,304 @@
|
||||||
|
import { create, decode, Header, Payload, verify } from "./mod.ts";
|
||||||
|
|
||||||
|
import {
|
||||||
|
assertEquals,
|
||||||
|
assertThrows,
|
||||||
|
assertThrowsAsync,
|
||||||
|
} from "../testing/asserts.ts";
|
||||||
|
|
||||||
|
const header: Header = {
|
||||||
|
alg: "HS256",
|
||||||
|
typ: "JWT",
|
||||||
|
};
|
||||||
|
|
||||||
|
const payload: Payload = {
|
||||||
|
name: "John Doe",
|
||||||
|
};
|
||||||
|
|
||||||
|
const key = "secret";
|
||||||
|
|
||||||
|
Deno.test({
|
||||||
|
name: "[jwt] create",
|
||||||
|
fn: async function () {
|
||||||
|
assertEquals(
|
||||||
|
await create("", key),
|
||||||
|
"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9..B0lmJDC8zSfMJstPqLdOAWfM265-5Svj0XrACZm8DKa1y6VJA0W7d0VoGGKJo0quKxWUdf1B1ueElNk2Yl_cLw",
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
await create({}, key),
|
||||||
|
"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.e30.dGumW8J3t2BlAwqqoisyWDC6ov2hRtjTAFHzd-Tlr4DUScaHG4OYqTHXLHEzd3hU5wy5xs87vRov6QzZnj410g",
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
await create({ foo: "bar" }, key),
|
||||||
|
"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.WePl7achkd0oGNB8XRF_LJwxlyiPZqpdNgdKpDboAjSTsWq-aOGNynTp8TOv8KjonFym8vwFwppXOLoLXbkIaQ",
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
await create("null", key),
|
||||||
|
"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.bnVsbA.tv7DbhvALc5Eq2sC61Y9IZlG2G15hvJoug9UO6iwmE_UZOLva8EC-9PURg7IIj6f-F9jFWix8vCn9WaAMHR1AA",
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
await create("[]", key),
|
||||||
|
"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.W10.BqmZ-tVI9a-HDx6PpMiBdMq6lzcaqO9sW6pImw-NRajCCmRrVi6IgMhEw7lvOG6sxhteceVMl8_xFRGverJJWw",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test({
|
||||||
|
name: "[jwt] verify",
|
||||||
|
fn: async function () {
|
||||||
|
assertEquals(
|
||||||
|
await verify(await create("", key, { header: header }), key, {
|
||||||
|
algorithm: "HS256",
|
||||||
|
}),
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
await verify(
|
||||||
|
await create("abc", key, { header: header }),
|
||||||
|
key,
|
||||||
|
{
|
||||||
|
algorithm: "HS256",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
"abc",
|
||||||
|
);
|
||||||
|
|
||||||
|
await assertEquals(
|
||||||
|
await verify(await create("null", key), key),
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
await assertEquals(
|
||||||
|
await verify(await create("true", key), key),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
await verify(
|
||||||
|
await create(payload, key, { header: header }),
|
||||||
|
key,
|
||||||
|
{
|
||||||
|
algorithm: "HS256",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
await assertEquals(
|
||||||
|
await verify(await create({}, key), key),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
await assertEquals(
|
||||||
|
await verify(await create("[]", key), key),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
await assertEquals(
|
||||||
|
await verify(await create(`["a", 1, true]`, key), key),
|
||||||
|
["a", 1, true],
|
||||||
|
);
|
||||||
|
|
||||||
|
await assertThrowsAsync(
|
||||||
|
async () => {
|
||||||
|
// payload = { "exp": false }
|
||||||
|
await verify(
|
||||||
|
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOmZhbHNlfQ.LXb8M9J6ar14CTq7shnqDMWmSsoH_zyIHiD44Rqd6uI",
|
||||||
|
key,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
"The token is invalid.",
|
||||||
|
);
|
||||||
|
|
||||||
|
await assertThrowsAsync(
|
||||||
|
async () => {
|
||||||
|
await verify("", key);
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
"The serialization is invalid.",
|
||||||
|
);
|
||||||
|
|
||||||
|
await assertThrowsAsync(
|
||||||
|
async () => {
|
||||||
|
await verify("invalid", key);
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
"The serialization is invalid.",
|
||||||
|
);
|
||||||
|
|
||||||
|
await assertThrowsAsync(
|
||||||
|
async () => {
|
||||||
|
await verify(
|
||||||
|
await create({
|
||||||
|
// @ts-ignore */
|
||||||
|
exp: "invalid",
|
||||||
|
}, key),
|
||||||
|
key,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
"The token is invalid.",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test({
|
||||||
|
name: "[jwt] decode",
|
||||||
|
fn: async function () {
|
||||||
|
assertEquals(
|
||||||
|
decode(
|
||||||
|
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.TVCeFl1nnZWUMQkAQKuSo_I97YeIZAS8T1gOkErT7F8",
|
||||||
|
),
|
||||||
|
{
|
||||||
|
header: { alg: "HS256", typ: "JWT" },
|
||||||
|
payload: {},
|
||||||
|
signature:
|
||||||
|
"4d509e165d679d959431090040ab92a3f23ded87886404bc4f580e904ad3ec5f",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assertThrows(
|
||||||
|
() => {
|
||||||
|
decode("aaa");
|
||||||
|
},
|
||||||
|
TypeError,
|
||||||
|
"The serialization is invalid.",
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThrows(
|
||||||
|
() => {
|
||||||
|
decode("a");
|
||||||
|
},
|
||||||
|
TypeError,
|
||||||
|
"Illegal base64url string!",
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThrows(
|
||||||
|
() => {
|
||||||
|
// "ImEi" === base64url("a")
|
||||||
|
decode("ImEi.ImEi.ImEi.ImEi");
|
||||||
|
},
|
||||||
|
TypeError,
|
||||||
|
"The serialization is invalid.",
|
||||||
|
);
|
||||||
|
|
||||||
|
const jwt =
|
||||||
|
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
|
||||||
|
const header: Header = {
|
||||||
|
alg: "HS256",
|
||||||
|
typ: "JWT",
|
||||||
|
};
|
||||||
|
const payload = {
|
||||||
|
sub: "1234567890",
|
||||||
|
name: "John Doe",
|
||||||
|
iat: 1516239022,
|
||||||
|
};
|
||||||
|
assertEquals(decode(jwt), {
|
||||||
|
header,
|
||||||
|
payload,
|
||||||
|
signature:
|
||||||
|
"49f94ac7044948c78a285d904f87f0a4c7897f7e8f3a4eb2255fda750b2cc397",
|
||||||
|
});
|
||||||
|
assertEquals(await create(payload, "your-256-bit-secret", { header }), jwt);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test({
|
||||||
|
name: "[jwt] expired token",
|
||||||
|
fn: async function () {
|
||||||
|
const payload = {
|
||||||
|
iss: "joe",
|
||||||
|
jti: "123456789abc",
|
||||||
|
exp: 20000,
|
||||||
|
};
|
||||||
|
const header: Header = {
|
||||||
|
alg: "HS256",
|
||||||
|
dummy: 100,
|
||||||
|
};
|
||||||
|
|
||||||
|
await assertThrowsAsync(
|
||||||
|
async () => {
|
||||||
|
await verify(await create({ exp: 0 }, key), key);
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
"The token is expired.",
|
||||||
|
);
|
||||||
|
|
||||||
|
await assertThrowsAsync(
|
||||||
|
async () => {
|
||||||
|
await verify(
|
||||||
|
await create(payload, key, { header }),
|
||||||
|
key,
|
||||||
|
{ algorithm: "HS256" },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
"The token is expired.",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test({
|
||||||
|
name: "[jwt] none algorithm",
|
||||||
|
fn: async function () {
|
||||||
|
const payload = {
|
||||||
|
iss: "joe",
|
||||||
|
jti: "123456789abc",
|
||||||
|
};
|
||||||
|
const header: Header = {
|
||||||
|
alg: "none",
|
||||||
|
dummy: 100,
|
||||||
|
};
|
||||||
|
const jwt = await create(payload, key, { header });
|
||||||
|
const validatedPayload = await verify(jwt, "keyIsIgnored", {
|
||||||
|
algorithm: "none",
|
||||||
|
});
|
||||||
|
assertEquals(validatedPayload, payload);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test({
|
||||||
|
name: "[jwt] HS256 algorithm",
|
||||||
|
fn: async function () {
|
||||||
|
const header: Header = {
|
||||||
|
alg: "HS256",
|
||||||
|
typ: "JWT",
|
||||||
|
};
|
||||||
|
const payload = {
|
||||||
|
sub: "1234567890",
|
||||||
|
name: "John Doe",
|
||||||
|
iat: 1516239022,
|
||||||
|
};
|
||||||
|
const jwt = await create(payload, key, { header });
|
||||||
|
const validatedPayload = await verify(jwt, key, { algorithm: "HS256" });
|
||||||
|
assertEquals(
|
||||||
|
jwt,
|
||||||
|
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o",
|
||||||
|
);
|
||||||
|
assertEquals(validatedPayload, payload);
|
||||||
|
assertThrowsAsync(
|
||||||
|
async () => {
|
||||||
|
const invalidJwt = // jwt with not supported crypto algorithm in alg header:
|
||||||
|
"eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.bQTnz6AuMJvmXXQsVPrxeQNvzDkimo7VNXxHeSBfClLufmCVZRUuyTwJF311JHuh";
|
||||||
|
await verify(invalidJwt, "", {
|
||||||
|
algorithm: "HS256",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
`The token's algorithm does not match the specified algorithm 'HS256'.`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test({
|
||||||
|
name: "[jwt] HS512 algorithm",
|
||||||
|
fn: async function () {
|
||||||
|
const header: Header = { alg: "HS512", typ: "JWT" };
|
||||||
|
const payload = {
|
||||||
|
sub: "1234567890",
|
||||||
|
name: "John Doe",
|
||||||
|
admin: true,
|
||||||
|
iat: 1516239022,
|
||||||
|
};
|
||||||
|
const jwt = await create(payload, key, { header });
|
||||||
|
const validatedPayload = await verify(jwt, key, { algorithm: "HS512" });
|
||||||
|
assertEquals(validatedPayload, payload);
|
||||||
|
},
|
||||||
|
});
|
Loading…
Reference in a new issue