mirror of
https://github.com/denoland/deno.git
synced 2024-12-25 08:39:09 -05:00
034ab48086
Co-authored-by: Tim Reichen <timreichen@users.noreply.github.com>
208 lines
5 KiB
TypeScript
208 lines
5 KiB
TypeScript
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}`;
|
|
}
|