Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add(std/jwt): Implement the new jwt module #7991

Merged
merged 96 commits into from
Oct 20, 2020
Merged
Show file tree
Hide file tree
Changes from 83 commits
Commits
Show all changes
96 commits
Select commit Hold shift + click to select a range
02f3eec
Setup module - Copy relevant files from djwt
timonson Oct 2, 2020
9844540
rename functions
timreichen Oct 2, 2020
d5bd740
remove RS256
timreichen Oct 2, 2020
ccfe528
remove JwtError, throw Error instances
timreichen Oct 2, 2020
8b42c39
relative std import specifiers
timreichen Oct 2, 2020
e908fd6
remove JSON types
timreichen Oct 2, 2020
bfd93a2
redundant file
timreichen Oct 2, 2020
9d1b309
rename tests
timreichen Oct 2, 2020
7fdf1a3
relative std module specifiers
timreichen Oct 2, 2020
7c5d342
rename Config type
timreichen Oct 2, 2020
10810c2
Remove assertNever
timonson Oct 2, 2020
9e2ce22
restructure tests
timreichen Oct 2, 2020
6df15a1
Merge branch 'std/jwt' of https://github.com/timonson/deno into std/jwt
timreichen Oct 2, 2020
8105c26
rename Header type
timreichen Oct 2, 2020
6920ff0
rename tests
timreichen Oct 2, 2020
a2ed5a6
restructure validate function
timreichen Oct 2, 2020
9a9fced
Create _util.ts and move helper functions to it
timonson Oct 2, 2020
8837788
change validate return value
timreichen Oct 2, 2020
7d84a4d
Merge branch 'std/jwt' of https://github.com/timonson/deno into std/jwt
timreichen Oct 2, 2020
f46de04
cleanup exports
timreichen Oct 2, 2020
6bcce2d
add string literals
timreichen Oct 2, 2020
882612d
remove exports
timreichen Oct 2, 2020
7653960
add default config values
timreichen Oct 2, 2020
6055cfe
Change export statements
timonson Oct 3, 2020
67ab762
restructure methods and exports
timreichen Oct 3, 2020
d7b4c61
toplevel static reservedWords
timreichen Oct 4, 2020
da590fd
cleanup
timreichen Oct 4, 2020
aab3f10
correct test names
timreichen Oct 4, 2020
4ca53a4
move tests
timreichen Oct 4, 2020
32d9758
move types, prevent circular imports
timreichen Oct 4, 2020
d3b5c67
rename functions
timreichen Oct 4, 2020
f0a6a74
split functionality
timreichen Oct 4, 2020
e31ab9d
split tests
timreichen Oct 4, 2020
495beb9
Clarify thrown errors
timonson Oct 4, 2020
6267e10
Fix typo
timonson Oct 4, 2020
8834f31
Change Algorithm[] type
timonson Oct 7, 2020
58d576f
Remove header.ts and improve types
timonson Oct 7, 2020
71ed952
Improve types and logic
timonson Oct 7, 2020
8c2ec43
Run deno fmt
timonson Oct 7, 2020
e585b1b
Add isObject to _utils.ts
timonson Oct 7, 2020
ae7a73c
Change return type of 'verify' to unknown
timonson Oct 7, 2020
93690b2
Update example
timonson Oct 7, 2020
7d5e9f3
Update example
timonson Oct 7, 2020
558aae6
Fix isTokenObject
timonson Oct 7, 2020
80adce2
Made necessary restructuring
timonson Oct 7, 2020
ae1c296
Change error message for crit
timonson Oct 7, 2020
67ce363
Improve validation
timonson Oct 8, 2020
098ae72
Add comments and made minor changes
timonson Oct 8, 2020
8c182d7
Move functions from _util.ts to validation.ts
timonson Oct 8, 2020
694bdff
Add comment
timonson Oct 8, 2020
dc95ec0
Change argument type for type predicates from unknown to any
timonson Oct 10, 2020
78d3333
various improvements
timreichen Oct 10, 2020
12f5a5f
fix tests
timreichen Oct 10, 2020
98ce8c4
rename param
timreichen Oct 11, 2020
3716d28
make async, rename parse to decode
timreichen Oct 11, 2020
849bd02
Merge pull request #1 from timonson/proposal-1
timreichen Oct 11, 2020
1354e6f
Add first README draft
timonson Oct 13, 2020
71b61d7
Merge branch 'master' into std/jwt
timonson Oct 13, 2020
90a2cc4
Remove base64 module and import base64 from /std/encoding
timonson Oct 13, 2020
c246060
update README.md
timreichen Oct 13, 2020
508d400
move encoder and fmt
timreichen Oct 13, 2020
e9dd016
update README.md
timreichen Oct 13, 2020
342ba92
update README.md
timreichen Oct 13, 2020
f509a9f
Merge branch 'update-readme' of https://github.com/timonson/deno into…
timreichen Oct 13, 2020
a044e64
Update README.md
timreichen Oct 13, 2020
0351f2b
Merge pull request #2 from timonson/update-readme
timreichen Oct 13, 2020
0feb734
Remove examples and testdata
timonson Oct 13, 2020
aa7cd7d
Don't stringify strings before converting to base64url
timonson Oct 14, 2020
bfbb28c
Fix tryToParsePayload
timonson Oct 14, 2020
b31f5bc
Assign decoder and move tryToParsePayload out of function
timonson Oct 14, 2020
f07591e
add more tests
timreichen Oct 14, 2020
cdc23a7
Merge branch 'std/jwt' of https://github.com/timonson/deno into std/jwt
timreichen Oct 14, 2020
f15c663
Change algorithm comment
timonson Oct 14, 2020
d03c4a2
adjust payload
timreichen Oct 15, 2020
0a159fe
Add Serialization to readme and improve comments
timonson Oct 15, 2020
12765ed
Add algorithm test for type string
timonson Oct 15, 2020
2d4e9e4
Add algorithm test for type string
timonson Oct 15, 2020
068448b
Add JSDoc
timonson Oct 15, 2020
7c39434
Change isTokenObject to reduce evaluations
timonson Oct 16, 2020
d326a71
Improve types
timonson Oct 16, 2020
eb1521a
Add comment to document reason for leeway
timonson Oct 16, 2020
87ad4c4
Improve error messages
timonson Oct 16, 2020
8d86fe7
Remove 'none' because it is redundant
timonson Oct 16, 2020
bda726f
Add type VerifyOptions
timonson Oct 16, 2020
a5a2f89
Fix JSDoc
timonson Oct 16, 2020
3a5926c
Add underscore to signature and algorithm files and change setExpirat…
timonson Oct 16, 2020
6c7db34
Merge the functions isTokenObject and decode
timonson Oct 17, 2020
04ecc8b
Change destructuring assignment
timonson Oct 17, 2020
594ecee
Remove createExpiration and add example to readme instead
timonson Oct 17, 2020
86fcd00
rename param
timreichen Oct 17, 2020
e301deb
update README.md
timreichen Oct 17, 2020
168ef35
update tests
timreichen Oct 17, 2020
e6cb730
tweaks
timreichen Oct 17, 2020
20e91f0
fmt
timreichen Oct 17, 2020
a5249b8
remove type assertions
timreichen Oct 17, 2020
549f299
remove obsolete code
timreichen Oct 17, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions std/jwt/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# jwt

Create and verify JSON Web Tokens.

## JSON Web Token

### create

Takes a `payload`, `key` and `header` and returns the url-safe encoded `jwt`.

```typescript
import { create } from "https://deno.land/std/jwt/mod.ts";

const payload = { foo: "bar" };
const key = "secret";

const jwt = await create(payload, key); // eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.4i-Q1Y0oDZunLgaorkqbYNcNfn5CgdF49UvJ7dUQ4GVTQvpsMLHABkZBWp9sghy3qVOsec6hOcu4RnbFkS30zQ
timonson marked this conversation as resolved.
Show resolved Hide resolved
```

**Specific algorithm**

```typescript
const jwt = await create(payload, key, { header: { alg: "HS256" } });
```

### verify

Takes a `jwt`, `key` and an optional `options` object and returns the `payload`
of the `jwt` if the `jwt` is valid. Otherwise it throws an `Error`.

```typescript
import { verify } from "https://deno.land/std/jwt/mod.ts";

const jwt =
"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.4i-Q1Y0oDZunLgaorkqbYNcNfn5CgdF49UvJ7dUQ4GVTQvpsMLHABkZBWp9sghy3qVOsec6hOcu4RnbFkS30zQ";
const key = "secret";

const payload = await verify(jwt, key); // { foo: "bar" }
```

**Specific algorithm**

```ts
const payload = await verify(jwt, key, { algorithm: "HS256" });
```

### decode

Takes a `jwt` to return an object with the `header`, `payload` and `signature`
properties if the `jwt` is valid. Otherwise it throws an `Error`.

```typescript
import { decode } from "https://deno.land/std/jwt/mod.ts";

const jwt =
"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.4i-Q1Y0oDZunLgaorkqbYNcNfn5CgdF49UvJ7dUQ4GVTQvpsMLHABkZBWp9sghy3qVOsec6hOcu4RnbFkS30zQ";

const { payload, signature, header } = await decode(jwt); // { header: { alg: "HS512", typ: "JWT" }, payload: { foo: "bar" }, signature: "e22f90d58d280d9ba72e06a8ae4a9b60d70d7e7e4281d178f54bc9edd510e0655342fa6c30b1c00646415a9f6c821cb7a953ac79cea139cbb84676c5912df4cd" }
```

## Expiration

### setExpiration

Takes either an `Date` object or a `number` (in seconds) as argument and returns
the number of seconds since January 1, 1970, 00:00:00 UTC

```typescript
import { setExpiration } from "https://deno.land/std/jwt/mod.ts";

// A specific date:
setExpiration(new Date("2025-07-01"));
// One hour from now:
setExpiration(60 * 60);
```

The optional **exp** claim in the payload 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 jwt = await create({ exp: setExpiration(60 * 60) }, "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)
20 changes: 20 additions & 0 deletions std/jwt/algorithm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
timonson marked this conversation as resolved.
Show resolved Hide resolved
* 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";

/*
* 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: Algorithm | Array<Exclude<Algorithm, "none">>,
jwtAlg: string,
): boolean {
return Array.isArray(algorithm)
? (algorithm as string[]).includes(jwtAlg)
: algorithm === jwtAlg;
}
11 changes: 11 additions & 0 deletions std/jwt/algorithm_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
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);
});
223 changes: 223 additions & 0 deletions std/jwt/mod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import type { Algorithm } 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;
}

/*
* Helper function: setExpiration()
* returns the number of seconds since January 1, 1970, 00:00:00 UTC
* @param number in seconds or Date object
*/
export function setExpiration(exp: number | Date): number {
timonson marked this conversation as resolved.
Show resolved Hide resolved
return Math.round(
(exp instanceof Date ? exp.getTime() : Date.now() + exp * 1000) / 1000,
);
}

function tryToParsePayload(input: string): unknown {
try {
return JSON.parse(input);
} catch {
return input;
}
}

/*
* Decodes a jwt into an { header, payload, signature } object
* @param jwt
*/
export function decode(
timonson marked this conversation as resolved.
Show resolved Hide resolved
jwt: string,
): {
header: unknown;
payload: unknown;
signature: unknown;
} {
const parsedArray = jwt
.split(".")
.map(base64url.decode)
.map((uint8Array, index) =>
index === 0
? JSON.parse(decoder.decode(uint8Array))
: index === 1
? tryToParsePayload(decoder.decode(uint8Array))
: convertUint8ArrayToHex(uint8Array)
);
if (parsedArray.length !== 3) {
throw TypeError("The serialization is invalid.");
}

return {
header: parsedArray[0],
payload: parsedArray[1],
signature: parsedArray[2],
};
}

export type TokenObject = {
header: Header;
payload: unknown;
signature: string;
};

/*
* @param object
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function isTokenObject(object: any): object is TokenObject {
if (
!(
typeof object?.signature === "string" &&
typeof object?.header?.alg === "string"
)
) {
throw new Error(`The jwt is invalid.`);
}
if (
typeof object?.payload?.exp === "number" && isExpired(object.payload.exp)
) {
throw RangeError("The jwt is expired.");
}
return true;
}

/*
* Verify a jwt
* @param jwt
* @param key
* @param object with property 'algorithm'
*/
export async function verify(
timonson marked this conversation as resolved.
Show resolved Hide resolved
jwt: string,
key: string,
{
algorithm = "HS512",
}: {
algorithm?: Algorithm | Array<Exclude<Algorithm, "none">>;
timonson marked this conversation as resolved.
Show resolved Hide resolved
} = {},
): Promise<unknown> {
const obj = decode(jwt);

if (isTokenObject(obj)) {
if (!verifyAlgorithm(algorithm, obj.header.alg)) {
throw new Error(
`The token's algorithm does not match the specified algorithm "${algorithm}".`,
);
}

const { header, payload, signature } = obj;

/*
* 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 obj.header) {
throw new Error(
"The 'crit' header parameter is currently not supported by this library.",
);
}

if (
!(await verifySignature({
signature,
key,
alg: header.alg,
signingInput: jwt.slice(0, jwt.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),
),
)
}`;
}

/*
* Create a jwt
* @param payload
* @param key
* @param object with property 'header'
*/
export async function create(
timonson marked this conversation as resolved.
Show resolved Hide resolved
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}`;
}
Loading