Skip to content

Commit

Permalink
Implement the Web Crypto API
Browse files Browse the repository at this point in the history
experimental
  • Loading branch information
timonson authored Aug 17, 2021
2 parents d5ca69b + 3ad41a4 commit 81f7244
Show file tree
Hide file tree
Showing 16 changed files with 535 additions and 509 deletions.
54 changes: 31 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,45 +4,50 @@ Create and verify JSON Web Tokens with deno.

## API

Please use the
[Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/generateKey)'s
method `generateKey` to generate a **secure** `CryptoKey`.

```typescript
const key = await crypto.subtle.generateKey(
{ name: "HMAC", hash: "SHA-512" },
true,
["sign", "verify"],
);
```

### create

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

```typescript
import { create } from "https://deno.land/x/djwt@$VERSION/mod.ts"
import { create } from "https://deno.land/x/djwt@$VERSION/mod.ts";

const jwt = await create({ alg: "HS512", typ: "JWT" }, { foo: "bar" }, "secret")
// eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.WePl7achkd0oGNB8XRF_LJwxlyiPZqpdNgdKpDboAjSTsWq-aOGNynTp8TOv8KjonFym8vwFwppXOLoLXbkIaQ
const jwt = await create({ alg: "HS512", typ: "JWT" }, { foo: "bar" }, key);
```

### verify

Takes a `jwt`, `key` and an `algorithm` and returns the `payload` of the `jwt`
if the `jwt` is valid. Otherwise it throws an `Error`.
Takes `jwt` and `CryptoKey` and returns the `Payload` of the `jwt` if the `jwt`
is valid. Otherwise it throws an `Error`.

```typescript
import { verify } from "https://deno.land/x/djwt@$VERSION/mod.ts"
import { verify } from "https://deno.land/x/djwt@$VERSION/mod.ts";

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

### decode

Takes a `jwt` and returns a 3-tuple `[header, payload, signature]` if the `jwt`
has a valid _serialization_. Otherwise it throws an `Error`.
Takes a `jwt` and returns a 3-tuple
`[header: unknown, payload: unknown, signature: Uint8Array]` 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"
import { decode } from "https://deno.land/x/djwt@$VERSION/mod.ts";

const [header, payload, signature] = decode(jwt)
// [
// { alg: "HS512", typ: "JWT" },
// { foo: "bar" },
// "59e3e5eda72191dd2818d07c5d117f2c9c3197288f66aa5d36074aa436e8023493b16abe68e18dca74e9f133aff0a8e89c5c..."
// ]
const [header, payload, signature] = decode(jwt);
```

### getNumericDate
Expand All @@ -54,9 +59,9 @@ This helper function simplifies setting a

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

## Claims
Expand All @@ -69,7 +74,7 @@ number containing a **NumericDate** value. This module checks if the current
date/time is before the expiration date/time listed in the **exp** claim.

```typescript
const jwt = await create(header, { exp: getNumericDate(60 * 60) }, "secret")
const jwt = await create(header, { exp: getNumericDate(60 * 60) }, "secret");
```

### Not Before (nbf)
Expand All @@ -83,10 +88,13 @@ jwt must not be accepted for processing. Its value must be a number containing a
The following signature and MAC algorithms have been implemented:

- HS256 (HMAC SHA-256)
- HS384 (HMAC SHA-384)
- HS512 (HMAC SHA-512)
- RS256 (RSASSA-PKCS1-v1_5 SHA-256)
- RS384 (RSASSA-PKCS1-v1_5 SHA-384)
- RS512 (RSASSA-PKCS1-v1_5 SHA-512)
- PS256 (RSASSA-PSS SHA-256)
- PS384 (RSASSA-PSS SHA-384)
- PS512 (RSASSA-PSS SHA-512)
- none ([_Unsecured JWTs_](https://tools.ietf.org/html/rfc7519#section-6)).

Expand Down
89 changes: 81 additions & 8 deletions algorithm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,91 @@
* https://www.rfc-editor.org/rfc/rfc7518
*/
export type Algorithm =
| "none"
| "HS256"
| "HS384"
| "HS512"
| "PS256"
| "PS384"
| "PS512"
| "RS256"
| "RS384"
| "RS512"
| "PS256"
| "PS512";
// The algorithm ECDSA will be implemented by Deno in version 1.14.0
// | "ES256"
// | "ES384"
// | "ES512"
| "none";

export type AlgorithmInput = Algorithm | Array<Exclude<Algorithm, "none">>;
export function verify(
alg: Algorithm,
key: CryptoKey | null,
): boolean {
if (alg === "none") {
if (key) throw new Error(`The alg '${alg}' does not allow a key.`);
else return true;
} else {
if (!key) throw new Error(`The alg '${alg}' demands a key.`);
const algorithm = getAlgorithm(alg);
if (key.algorithm.name === algorithm.name) {
if (
// Deno's type CryptoKey is still buggy, therefore type assertions.
// They will fix CryptoKey with the next release!
(key.algorithm as { hash?: { name: string } }).hash?.name &&
(key.algorithm as { hash?: { name: string } }).hash?.name ===
algorithm.hash.name
) {
return true;
// } else if (
// (key.algorithm as { namedCurve?: string }).namedCurve &&
// (key.algorithm as { namedCurve?: string }).namedCurve ===
// algorithm.namedCurve
// ) {
// return true;
}
}
}
return false;
}

export function verify(algorithm: AlgorithmInput, jwtAlg: string): boolean {
return Array.isArray(algorithm)
? (algorithm as string[]).includes(jwtAlg)
: algorithm === jwtAlg;
export function getAlgorithm(alg: Algorithm) {
switch (alg) {
case "HS256":
return { hash: { name: "SHA-256" }, name: "HMAC" };
case "HS384":
return { hash: { name: "SHA-384" }, name: "HMAC" };
case "HS512":
return { hash: { name: "SHA-512" }, name: "HMAC" };
case "PS256":
return {
hash: { name: "SHA-256" },
name: "RSA-PSS",
saltLength: 256 >> 3,
};
case "PS384":
return {
hash: { name: "SHA-384" },
name: "RSA-PSS",
saltLength: 384 >> 3,
};
case "PS512":
return {
hash: { name: "SHA-512" },
name: "RSA-PSS",
saltLength: 512 >> 3,
};
case "RS256":
return { hash: { name: "SHA-256" }, name: "RSASSA-PKCS1-v1_5" };
case "RS384":
return { hash: { name: "SHA-384" }, name: "RSASSA-PKCS1-v1_5" };
case "RS512":
return { hash: { name: "SHA-512" }, name: "RSASSA-PKCS1-v1_5" };
// case "ES256":
// return { hash: { name: "SHA-256" }, name: "ECDSA", namedCurve: "P-256" };
// case "ES384":
// return { hash: { name: "SHA-384" }, name: "ECDSA", namedCurve: "P-384" };
// case "ES512":
// return { hash: { name: "SHA-512" }, name: "ECDSA", namedCurve: "P-521" };
default:
throw new Error(`The jwt's alg '${alg}' is not supported.`);
}
}
9 changes: 1 addition & 8 deletions deps.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1 @@
export * as base64url from "https://deno.land/[email protected]/encoding/base64url.ts";
export {
decodeString as convertHexToUint8Array,
encodeToString as convertUint8ArrayToHex,
} from "https://deno.land/[email protected]/encoding/hex.ts";
export { HmacSha256 } from "https://deno.land/[email protected]/hash/sha256.ts";
export { HmacSha512 } from "https://deno.land/[email protected]/hash/sha512.ts";
export { RSA } from "https://deno.land/x/[email protected]/rsa.ts";
export * as base64url from "https://deno.land/[email protected]/encoding/base64url.ts";
27 changes: 0 additions & 27 deletions examples/certs/private.pem

This file was deleted.

9 changes: 0 additions & 9 deletions examples/certs/public.pem

This file was deleted.

4 changes: 1 addition & 3 deletions examples/example_deps.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
export { serve } from "https://deno.land/[email protected]/http/server.ts";
export { decode, encode } from "https://deno.land/[email protected]/encoding/utf8.ts";
export { dirname, fromFileUrl } from "https://deno.land/[email protected]/path/mod.ts";
export { serve } from "https://deno.land/[email protected]/http/server.ts";
13 changes: 8 additions & 5 deletions examples/hmac_example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ import { serve } from "./example_deps.ts";

import type { Header, Payload } from "../mod.ts";

const key = "your-secret";
const key = await crypto.subtle.generateKey(
{ name: "HMAC", hash: "SHA-512" },
true,
["sign", "verify"],
);
const payload: Payload = {
iss: "joe",
exp: getNumericDate(60),
};
const header: Header = {
alg: "HS256",
alg: "HS512",
typ: "JWT",
};

Expand All @@ -19,8 +23,7 @@ for await (const req of serve("0.0.0.0:8000")) {
req.respond({ body: (await create(header, payload, key)) + "\n" });
} else {
const jwt = new TextDecoder().decode(await Deno.readAll(req.body));
await verify(jwt, key, "HS256").then(() =>
req.respond({ body: "Valid JWT\n" })
).catch(() => req.respond({ body: "Invalid JWT\n", status: 401 }));
await verify(jwt, key).then(() => req.respond({ body: "Valid JWT\n" }))
.catch(() => req.respond({ body: "Invalid JWT\n", status: 401 }));
}
}
27 changes: 19 additions & 8 deletions examples/rsa_example.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,41 @@
import { create, verify } from "../mod.ts";
import { dirname, fromFileUrl, serve } from "./example_deps.ts";
import { serve } from "./example_deps.ts";

import type { Header, Payload } from "../mod.ts";

const moduleDir = dirname(fromFileUrl(import.meta.url));
const publicKey = await Deno.readTextFile(moduleDir + "/certs/public.pem");
const privateKey = await Deno.readTextFile(moduleDir + "/certs/private.pem");
const key = await window.crypto.subtle.generateKey(
{
name: "RSASSA-PKCS1-v1_5",
modulusLength: 4096,
publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-384",
},
true,
["verify", "sign"],
);

const payload: Payload = {
sub: "1234567890",
name: "John Doe",
admin: true,
iat: 1516239022,
};
const header: Header = {
alg: "RS256",
alg: "RS384",
typ: "JWT",
};

console.log("server is listening at 0.0.0.0:8000");
for await (const req of serve("0.0.0.0:8000")) {
if (req.method === "GET") {
req.respond({ body: (await create(header, payload, privateKey)) + "\n" });
req.respond({
body: (await create(header, payload, key.privateKey)) + "\n",
});
} else {
const jwt = new TextDecoder().decode(await Deno.readAll(req.body));
await verify(jwt, publicKey, "RS256").then(() =>
await verify(jwt, key.publicKey).then(() =>
req.respond({ body: "Valid JWT\n" })
).catch(() => req.respond({ body: "Invalid JWT\n", status: 401 }));
)
.catch(() => req.respond({ body: "Invalid JWT\n", status: 401 }));
}
}
Loading

0 comments on commit 81f7244

Please sign in to comment.