diff --git a/std/jwt/README.md b/std/jwt/README.md deleted file mode 100644 index fe895dafca..0000000000 --- a/std/jwt/README.md +++ /dev/null @@ -1,90 +0,0 @@ -# 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@$STD_VERSION/jwt/mod.ts"; - -const payload = { foo: "bar" }; -const key = "secret"; - -const token = await create(payload, key); // eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.WePl7achkd0oGNB8XRF_LJwxlyiPZqpdNgdKpDboAjSTsWq-aOGNynTp8TOv8KjonFym8vwFwppXOLoLXbkIaQ -``` - -**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@$STD_VERSION/jwt/mod.ts"; - -const token = - "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.WePl7achkd0oGNB8XRF_LJwxlyiPZqpdNgdKpDboAjSTsWq-aOGNynTp8TOv8KjonFym8vwFwppXOLoLXbkIaQ"; -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@$STD_VERSION/jwt/mod.ts"; - -const token = - "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.WePl7achkd0oGNB8XRF_LJwxlyiPZqpdNgdKpDboAjSTsWq-aOGNynTp8TOv8KjonFym8vwFwppXOLoLXbkIaQ"; - -const { payload, signature, header } = await decode(token); // { header: { alg: "HS512", typ: "JWT" }, payload: { foo: "bar" }, signature: "59e3e5eda72191dd2818d07c5d117f2c9c3197288f66aa5d36074aa436e8023493b16abe68e18dca74e9f133aff0a8e89c5ca6f2fc05c29a5738ba0b5db90869" } -``` - -## 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) diff --git a/std/jwt/_algorithm.ts b/std/jwt/_algorithm.ts deleted file mode 100644 index c9c5257e10..0000000000 --- a/std/jwt/_algorithm.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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>; -/** - * 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; -} diff --git a/std/jwt/_algorithm_test.ts b/std/jwt/_algorithm_test.ts deleted file mode 100644 index 99583bd79c..0000000000 --- a/std/jwt/_algorithm_test.ts +++ /dev/null @@ -1,11 +0,0 @@ -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); -}); diff --git a/std/jwt/_signature.ts b/std/jwt/_signature.ts deleted file mode 100644 index 81c1309d14..0000000000 --- a/std/jwt/_signature.ts +++ /dev/null @@ -1,63 +0,0 @@ -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 { - 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 { - return signature === (await encrypt(algorithm, key, signingInput)); -} diff --git a/std/jwt/_signature_test.ts b/std/jwt/_signature_test.ts deleted file mode 100644 index c02f0f4bc3..0000000000 --- a/std/jwt/_signature_test.ts +++ /dev/null @@ -1,46 +0,0 @@ -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); -}); diff --git a/std/jwt/mod.ts b/std/jwt/mod.ts deleted file mode 100644 index 09485c8c63..0000000000 --- a/std/jwt/mod.ts +++ /dev/null @@ -1,208 +0,0 @@ -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 { - 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 { - const signingInput = createSigningInput(header, payload); - const signature = await createSignature(header.alg, key, signingInput); - - return `${signingInput}.${signature}`; -} diff --git a/std/jwt/test.ts b/std/jwt/test.ts deleted file mode 100644 index deaa857e28..0000000000 --- a/std/jwt/test.ts +++ /dev/null @@ -1,304 +0,0 @@ -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); - }, -});