Skip to content

Commit

Permalink
Merge pull request #46 from timonson/patch-validate
Browse files Browse the repository at this point in the history
Add function validate
  • Loading branch information
timonson authored Jan 30, 2021
2 parents eb22a8b + 21da69b commit 6d7896f
Show file tree
Hide file tree
Showing 8 changed files with 175 additions and 88 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,21 @@ 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"

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
Expand Down
10 changes: 5 additions & 5 deletions deps.ts
Original file line number Diff line number Diff line change
@@ -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";
export { RSA } from "https://deno.land/x/[email protected].8/rsa.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/[email protected].9/rsa.ts";
6 changes: 3 additions & 3 deletions examples/example_deps.ts
Original file line number Diff line number Diff line change
@@ -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";
102 changes: 49 additions & 53 deletions mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> {
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
Expand All @@ -66,89 +70,81 @@ 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.`);
}

/*
* JWT §7.2: Verify that the resulting octet sequence is a UTF-8-encoded
* 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(
jwt: string,
key: string,
algorithm: AlgorithmInput,
): Promise<Payload> {
const { header, payload, signature } = decode(jwt);
const { header, payload, signature } = validate(decode(jwt));

if (!verifyAlgorithm(algorithm, header.alg)) {
throw new Error(
`The jwt'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,
Expand Down
4 changes: 2 additions & 2 deletions tests/signature_test.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 6d7896f

Please sign in to comment.