From ab7eb1472de85b58aea2b485b5cc0323b232cb53 Mon Sep 17 00:00:00 2001 From: timonson Date: Mon, 25 Jan 2021 15:19:47 +0100 Subject: [PATCH 1/5] Add function validate --- mod.ts | 102 ++++++++++++++++----------------- tests/signature_test.ts | 4 +- tests/test.ts | 122 ++++++++++++++++++++++++++++++++++------ 3 files changed, 156 insertions(+), 72 deletions(-) diff --git a/mod.ts b/mod.ts index 46c2a35..7e7001a 100644 --- a/mod.ts +++ b/mod.ts @@ -52,12 +52,16 @@ function isTooEarly(nbf: number, leeway = 0): boolean { return nbf - leeway > Date.now() / 1000; } -function isObject(obj: unknown) { +function isObject(obj: unknown): obj is Record { return ( obj !== null && typeof obj === "object" && Array.isArray(obj) === false ); } +function is3Tuple(arr: any[]): arr is [unknown, unknown, unknown] { + return arr.length === 3; +} + function hasInvalidTimingClaims(...claimValues: unknown[]): boolean { return claimValues.some((claimValue) => claimValue !== undefined ? typeof claimValue !== "number" : false @@ -66,35 +70,38 @@ function hasInvalidTimingClaims(...claimValues: unknown[]): boolean { export function decode( jwt: string, -): { +): [header: unknown, payload: unknown, signature: unknown] { + try { + const arr = jwt + .split(".") + .map(base64url.decode) + .map((uint8Array, index) => { + switch (index) { + case 0: + case 1: + return JSON.parse(decoder.decode(uint8Array)); + case 2: + return convertUint8ArrayToHex(uint8Array); + } + }); + if (is3Tuple(arr)) return arr; + else throw new Error(); + } catch { + throw TypeError("The serialization of the jwt is invalid."); + } +} + +export function validate([header, payload, signature]: [any, any, any]): { header: Header; payload: Payload; signature: string; } { - const [header, payload, signature] = jwt - .split(".") - .map(base64url.decode) - .map((uint8Array, index) => { - switch (index) { - case 0: - case 1: - try { - return JSON.parse(decoder.decode(uint8Array)); - } catch { - break; - } - case 2: - return convertUint8ArrayToHex(uint8Array); - } - throw TypeError("The serialization is invalid."); - }); - if (typeof signature !== "string") { - throw new Error(`The signature is missing.`); + throw new Error(`The signature of the jwt must be a string.`); } if (typeof header?.alg !== "string") { - throw new Error(`The header 'alg' parameter must be a string.`); + throw new Error(`The header 'alg' parameter of the jwt must be a string.`); } /* @@ -102,27 +109,27 @@ export function decode( * representation of a completely valid JSON object conforming to RFC 7159; * let the JWT Claims Set be this JSON object. */ - if (!isObject(payload)) { + if (isObject(payload)) { + if (hasInvalidTimingClaims(payload.exp, payload.nbf)) { + throw new Error(`The jwt has an invalid 'exp' or 'nbf' claim.`); + } + + if (typeof payload.exp === "number" && isExpired(payload.exp, 1)) { + throw RangeError("The jwt is expired."); + } + + if (typeof payload.nbf === "number" && isTooEarly(payload.nbf, 1)) { + throw RangeError("The jwt is used too early."); + } + + return { + header, + payload, + signature, + }; + } else { throw new Error(`The jwt claims set is not a JSON object.`); } - - if (hasInvalidTimingClaims(payload.exp, payload.nbf)) { - throw new Error(`The jwt has an invalid 'exp' or 'nbf' claim.`); - } - - if (typeof payload.exp === "number" && isExpired(payload.exp, 1)) { - throw RangeError("The jwt is expired."); - } - - if (typeof payload.nbf === "number" && isTooEarly(payload.nbf, 1)) { - throw RangeError("The jwt is used too early."); - } - - return { - header, - payload, - signature, - }; } export async function verify( @@ -130,7 +137,7 @@ export async function verify( key: string, algorithm: AlgorithmInput, ): Promise { - const { header, payload, signature } = decode(jwt); + const { header, payload, signature } = validate(decode(jwt)); if (!verifyAlgorithm(algorithm, header.alg)) { throw new Error( @@ -138,17 +145,6 @@ export async function verify( ); } - /* - * 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, diff --git a/tests/signature_test.ts b/tests/signature_test.ts index 5449190..cfa26c9 100644 --- a/tests/signature_test.ts +++ b/tests/signature_test.ts @@ -1,5 +1,5 @@ import { assertEquals } from "./test_deps.ts"; -import { create, decode } from "../mod.ts"; +import { create, decode, validate } from "../mod.ts"; import { convertHexToBase64url, @@ -33,7 +33,7 @@ Deno.test("[jwt] create signature", async function () { Deno.test("[jwt] verify signature", async function () { const jwt = await create({ alg: "HS512", typ: "JWT" }, {}, key); - const { header, signature } = decode(jwt); + const { header, signature } = validate(decode(jwt)); const validSignature = await verifySignature({ signature, key, diff --git a/tests/test.ts b/tests/test.ts index f171be7..56f6d3e 100644 --- a/tests/test.ts +++ b/tests/test.ts @@ -4,6 +4,7 @@ import { getNumericDate, Header, Payload, + validate, verify, } from "../mod.ts"; @@ -105,7 +106,7 @@ Deno.test({ await verify("", key, "HS512"); }, Error, - "The serialization is invalid.", + "The serialization of the jwt is invalid.", ); await assertThrowsAsync( @@ -113,7 +114,7 @@ Deno.test({ await verify("invalid", key, "HS512"); }, Error, - "The serialization is invalid.", + "The serialization of the jwt is invalid.", ); await assertThrowsAsync( @@ -141,7 +142,7 @@ Deno.test({ ); }, Error, - "The serialization is invalid.", + "The serialization of the jwt is invalid.", ); await assertThrowsAsync( async () => { @@ -152,7 +153,7 @@ Deno.test({ ); }, Error, - "The serialization is invalid.", + "The serialization of the jwt is invalid.", ); await assertThrowsAsync( @@ -210,19 +211,19 @@ Deno.test({ decode( "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.TVCeFl1nnZWUMQkAQKuSo_I97YeIZAS8T1gOkErT7F8", ), - { - header: { alg: "HS256", typ: "JWT" }, - payload: {}, - signature: - "4d509e165d679d959431090040ab92a3f23ded87886404bc4f580e904ad3ec5f", - }, + [ + { alg: "HS256", typ: "JWT" }, + {}, + + "4d509e165d679d959431090040ab92a3f23ded87886404bc4f580e904ad3ec5f", + ], ); assertThrows( () => { decode("aaa"); }, TypeError, - "The serialization is invalid.", + "The serialization of the jwt is invalid.", ); assertThrows( @@ -230,7 +231,7 @@ Deno.test({ decode("a"); }, TypeError, - "Illegal base64url string!", + "The serialization of the jwt is invalid.", ); assertThrows( @@ -239,7 +240,95 @@ Deno.test({ decode("ImEi.ImEi.ImEi.ImEi"); }, TypeError, - "The serialization is invalid.", + "The serialization of the jwt 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, + "49f94ac7044948c78a285d904f87f0a4c7897f7e8f3a4eb2255fda750b2cc397", + ]); + assertEquals( + await create(header, payload, "your-256-bit-secret"), + jwt, + ); + }, +}); + +Deno.test({ + name: "[jwt] validate", + fn: async function () { + assertEquals( + validate( + [ + { alg: "HS256", typ: "JWT" }, + { exp: 1111111111111111111111111111 }, + "", + ], + ), + { + header: { alg: "HS256", typ: "JWT" }, + payload: { exp: 1111111111111111111111111111 }, + signature: "", + }, + ); + assertThrows( + () => { + validate([, , null]); + }, + Error, + "The signature of the jwt must be a string.", + ); + + assertThrows( + () => { + validate([null, {}, ""]); + }, + Error, + "The header 'alg' parameter of the jwt must be a string.", + ); + + assertThrows( + () => { + validate([{ alg: "HS256", typ: "JWT" }, [], ""]); + }, + Error, + "The jwt claims set is not a JSON object.", + ); + + assertThrows( + () => { + validate([{ alg: "HS256" }, { exp: "" }, ""]); + }, + Error, + "The jwt has an invalid 'exp' or 'nbf' claim.", + ); + + assertThrows( + () => { + validate([{ alg: "HS256" }, { exp: 1 }, ""]); + }, + Error, + "The jwt is expired.", + ); + + assertThrows( + () => { + validate([{ alg: "HS256" }, { nbf: 1111111111111111111111111111 }, ""]); + }, + Error, + "The jwt is used too early.", ); const jwt = @@ -253,12 +342,11 @@ Deno.test({ name: "John Doe", iat: 1516239022, }; - assertEquals(decode(jwt), { + assertEquals(decode(jwt), [ header, payload, - signature: - "49f94ac7044948c78a285d904f87f0a4c7897f7e8f3a4eb2255fda750b2cc397", - }); + "49f94ac7044948c78a285d904f87f0a4c7897f7e8f3a4eb2255fda750b2cc397", + ]); assertEquals( await create(header, payload, "your-256-bit-secret"), jwt, From b120653047b80e3cb7c14b6d48cf9f986d521d8c Mon Sep 17 00:00:00 2001 From: timonson Date: Mon, 25 Jan 2021 15:20:21 +0100 Subject: [PATCH 2/5] Use latest version of deno --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index aa54552..6683c75 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,5 +9,5 @@ jobs: - uses: actions/checkout@master - uses: denolib/setup-deno@master with: - deno-version: v1.7.0 + deno-version: x - run: deno test -A From 297bee53ffbf5dc2b6da1620755e199992d27be7 Mon Sep 17 00:00:00 2001 From: timonson Date: Sun, 31 Jan 2021 00:32:18 +0100 Subject: [PATCH 3/5] Update documentation for function decode --- README.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c8874bf..ee99597 100644 --- a/README.md +++ b/README.md @@ -28,9 +28,8 @@ const payload = await verify(jwt, "secret", "HS512") // { foo: "bar" } ### decode -Takes a `jwt` and returns an object with the `header`, `payload` and `signature` -properties if the `jwt` is valid (without signature verification). Otherwise it -throws an `Error`. +Takes a `jwt` and returns a 3-tuple `[header, payload, signature]` if the `jwt` +has a valid _serialization_. Otherwise it throws an `Error`. ```typescript import { decode } from "https://deno.land/x/djwt@$VERSION/mod.ts" @@ -38,8 +37,12 @@ import { decode } from "https://deno.land/x/djwt@$VERSION/mod.ts" const jwt = "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.WePl7achkd0oGNB8XRF_LJwxlyiPZqpdNgdKpDboAjSTsWq-aOGNynTp8TOv8KjonFym8vwFwppXOLoLXbkIaQ" -const { payload, signature, header } = decode(jwt) -// { header: { alg: "HS512", typ: "JWT" }, payload: { foo: "bar" }, signature: "59e3e5eda72191dd2818d07c5d117f2c9c3197288f66aa5d36074aa436e8023493b16abe68e18dca74e9f133aff0a8e89c5ca6f2fc05c29a5738ba0b5db90869" } +const [payload, signature, header] = decode(jwt) +// [ +// { alg: "HS512", typ: "JWT" }, +// { foo: "bar" }, +// "59e3e5eda72191dd2818d07c5d117f2c9c3197288f66aa5d36074aa436e8023493b16abe68e18dca74e9f133aff0a8e89c5c..." +// ] ``` ### getNumericDate From b416bae3b3fdd661b2e7ec4880498bc8ccd780fb Mon Sep 17 00:00:00 2001 From: timonson Date: Sun, 31 Jan 2021 00:37:20 +0100 Subject: [PATCH 4/5] Bump versions --- deps.ts | 8 ++++---- examples/example_deps.ts | 6 +++--- tests/test_deps.ts | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/deps.ts b/deps.ts index 8846c9c..4c71472 100644 --- a/deps.ts +++ b/deps.ts @@ -1,8 +1,8 @@ -export * as base64url from "https://deno.land/std@0.84.0/encoding/base64url.ts"; +export * as base64url from "https://deno.land/std@0.85.0/encoding/base64url.ts"; export { decodeString as convertHexToUint8Array, encodeToString as convertUint8ArrayToHex, -} from "https://deno.land/std@0.84.0/encoding/hex.ts"; -export { HmacSha256 } from "https://deno.land/std@0.84.0/hash/sha256.ts"; -export { HmacSha512 } from "https://deno.land/std@0.84.0/hash/sha512.ts"; +} from "https://deno.land/std@0.85.0/encoding/hex.ts"; +export { HmacSha256 } from "https://deno.land/std@0.85.0/hash/sha256.ts"; +export { HmacSha512 } from "https://deno.land/std@0.85.0/hash/sha512.ts"; export { RSA } from "https://deno.land/x/god_crypto@v1.4.8/rsa.ts"; diff --git a/examples/example_deps.ts b/examples/example_deps.ts index 8ffb07f..59873e9 100644 --- a/examples/example_deps.ts +++ b/examples/example_deps.ts @@ -1,3 +1,3 @@ -export { serve } from "https://deno.land/std@0.84.0/http/server.ts"; -export { decode, encode } from "https://deno.land/std@0.84.0/encoding/utf8.ts"; -export { dirname, fromFileUrl } from "https://deno.land/std@0.84.0/path/mod.ts"; +export { serve } from "https://deno.land/std@0.85.0/http/server.ts"; +export { decode, encode } from "https://deno.land/std@0.85.0/encoding/utf8.ts"; +export { dirname, fromFileUrl } from "https://deno.land/std@0.85.0/path/mod.ts"; diff --git a/tests/test_deps.ts b/tests/test_deps.ts index 8ce7cae..a058430 100644 --- a/tests/test_deps.ts +++ b/tests/test_deps.ts @@ -2,6 +2,6 @@ export { assertEquals, assertThrows, assertThrowsAsync, -} from "https://deno.land/std@0.84.0/testing/asserts.ts"; +} from "https://deno.land/std@0.85.0/testing/asserts.ts"; -export { dirname, fromFileUrl } from "https://deno.land/std@0.84.0/path/mod.ts"; +export { dirname, fromFileUrl } from "https://deno.land/std@0.85.0/path/mod.ts"; From 21da69be0462ee136d6c0dcaa03d9b12c8503292 Mon Sep 17 00:00:00 2001 From: timonson Date: Sun, 31 Jan 2021 00:38:15 +0100 Subject: [PATCH 5/5] Bump versions --- deps.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps.ts b/deps.ts index 4c71472..9a6ba05 100644 --- a/deps.ts +++ b/deps.ts @@ -5,4 +5,4 @@ export { } from "https://deno.land/std@0.85.0/encoding/hex.ts"; export { HmacSha256 } from "https://deno.land/std@0.85.0/hash/sha256.ts"; export { HmacSha512 } from "https://deno.land/std@0.85.0/hash/sha512.ts"; -export { RSA } from "https://deno.land/x/god_crypto@v1.4.8/rsa.ts"; +export { RSA } from "https://deno.land/x/god_crypto@v1.4.9/rsa.ts";