summaryrefslogtreecommitdiff
path: root/std/jwt/mod.ts
diff options
context:
space:
mode:
authortimonson <54777088+timonson@users.noreply.github.com>2020-10-20 05:08:34 +0200
committerGitHub <noreply@github.com>2020-10-20 14:08:34 +1100
commit034ab48086557af00216ffe311c71ad4eb0ec4d5 (patch)
tree292abc572ed68eb52c1bc773e64f820497e065df /std/jwt/mod.ts
parent992c2a436e5fe371807dd43bd293bb811fd529e7 (diff)
feat(std/jwt): add a JSON Web Token library (#7991)
Co-authored-by: Tim Reichen <timreichen@users.noreply.github.com>
Diffstat (limited to 'std/jwt/mod.ts')
-rw-r--r--std/jwt/mod.ts208
1 files changed, 208 insertions, 0 deletions
diff --git a/std/jwt/mod.ts b/std/jwt/mod.ts
new file mode 100644
index 000000000..09485c8c6
--- /dev/null
+++ b/std/jwt/mod.ts
@@ -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}`;
+}