@cfworker/jwt 1.12.2 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  Tiny lib for decoding JWTs and verifying signatures, using native crypto APIs.
10
10
 
11
- Currently supports `alg:'RS256'` only.
11
+ Currently supports RS256, RS384, and RS512.
12
12
 
13
13
  ```js
14
14
  const jwt = request.headers.get('Authorization');
package/dist/algs.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export declare const algToHash: Record<string, string>;
2
+ export declare const algs: string[];
package/dist/algs.js ADDED
@@ -0,0 +1,6 @@
1
+ export const algToHash = {
2
+ RS256: 'SHA-256',
3
+ RS384: 'SHA-384',
4
+ RS512: 'SHA-512'
5
+ };
6
+ export const algs = Object.keys(algToHash);
package/dist/decode.js CHANGED
@@ -1,10 +1,11 @@
1
- import { decode } from '@cfworker/base64url';
1
+ import { base64url } from 'rfc4648';
2
2
  export function decodeJwt(token) {
3
3
  const [header, payload, signature] = token.split('.');
4
+ const decoder = new TextDecoder();
4
5
  return {
5
- header: JSON.parse(decode(header)),
6
- payload: JSON.parse(decode(payload)),
7
- signature: decode(signature),
6
+ header: JSON.parse(decoder.decode(base64url.parse(header, { loose: true }))),
7
+ payload: JSON.parse(decoder.decode(base64url.parse(payload, { loose: true }))),
8
+ signature: base64url.parse(signature, { loose: true }),
8
9
  raw: { header, payload, signature }
9
10
  };
10
11
  }
@@ -0,0 +1,2 @@
1
+ import { IssuerMetadata } from './types';
2
+ export declare function getIssuerMetadata(issuer: string): Promise<IssuerMetadata>;
@@ -0,0 +1,12 @@
1
+ export async function getIssuerMetadata(issuer) {
2
+ const url = new URL(issuer);
3
+ if (!url.pathname.endsWith('/')) {
4
+ url.pathname += '/';
5
+ }
6
+ url.pathname += '.well-known/openid-configuration';
7
+ const response = await fetch(url.href);
8
+ if (!response.ok) {
9
+ throw new Error(`Error loading OpenID discovery document at ${url.href}. ${response.status} ${response.statusText}`);
10
+ }
11
+ return response.json();
12
+ }
package/dist/jwks.js CHANGED
@@ -1,9 +1,18 @@
1
+ import { algToHash } from './algs.js';
2
+ import { getIssuerMetadata } from './discovery.js';
1
3
  export async function getJwks(issuer) {
2
- const url = new URL(issuer);
3
- if (!url.pathname.endsWith('/')) {
4
- url.pathname += '/';
4
+ const issuerMetadata = await getIssuerMetadata(issuer);
5
+ let url;
6
+ if (issuerMetadata.jwks_uri) {
7
+ url = new URL(issuerMetadata.jwks_uri);
8
+ }
9
+ else {
10
+ url = new URL(issuer);
11
+ if (!url.pathname.endsWith('/')) {
12
+ url.pathname += '/';
13
+ }
14
+ url.pathname += '.well-known/jwks.json';
5
15
  }
6
- url.pathname += '.well-known/jwks.json';
7
16
  const response = await fetch(url.href);
8
17
  if (!response.ok) {
9
18
  throw new Error(`Error loading jwks at ${url.href}. ${response.status} ${response.statusText}`);
@@ -12,16 +21,16 @@ export async function getJwks(issuer) {
12
21
  }
13
22
  const importedKeys = {};
14
23
  export async function importKey(iss, jwk) {
15
- const input = {
16
- kty: 'RSA',
17
- e: 'AQAB',
18
- n: jwk.n,
19
- alg: 'RS256',
20
- ext: true
21
- };
22
- const key = await crypto.subtle.importKey('jwk', input, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, false, ['verify']);
24
+ if (jwk.kty !== 'RSA') {
25
+ throw new Error(`Unsupported jwk key type (kty) "${jwk.kty}": Full JWK was ${JSON.stringify(jwk)}`);
26
+ }
27
+ const hash = jwk.alg ? algToHash[jwk.alg] : 'SHA-256';
28
+ if (!hash) {
29
+ throw new Error(`Unsupported jwk Algorithm (alg) "${jwk.alg}": Full JWK was ${JSON.stringify(jwk)}`);
30
+ }
31
+ const key = await crypto.subtle.importKey('jwk', jwk, { name: 'RSASSA-PKCS1-v1_5', hash }, false, ['verify']);
23
32
  importedKeys[iss] = importedKeys[iss] || {};
24
- importedKeys[iss][jwk.kid || 'default'] = key;
33
+ importedKeys[iss][jwk.kid ?? 'default'] = key;
25
34
  }
26
35
  export async function getKey(decoded) {
27
36
  let { header: { kid = 'default' }, payload: { iss } } = decoded;
package/dist/parse.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { JwtParseResult } from './types.js';
2
- export declare function parseJwt(encodedToken: string, issuer: string, audience: string): Promise<JwtParseResult>;
1
+ import { DecodedJwt, JwtParseResult } from './types.js';
2
+ export declare function parseJwt(encodedToken: string, issuer: string, audience: string, resolveKey?: (decoded: DecodedJwt) => Promise<CryptoKey | null>): Promise<JwtParseResult>;
package/dist/parse.js CHANGED
@@ -1,7 +1,9 @@
1
+ import { algs, algToHash } from './algs.js';
1
2
  import { decodeJwt } from './decode.js';
2
3
  import { getKey } from './jwks.js';
3
4
  import { verifyJwtSignature } from './verify.js';
4
- export async function parseJwt(encodedToken, issuer, audience) {
5
+ const skewMs = 30 * 1000;
6
+ export async function parseJwt(encodedToken, issuer, audience, resolveKey = getKey) {
5
7
  let decoded;
6
8
  try {
7
9
  decoded = decodeJwt(encodedToken);
@@ -9,57 +11,120 @@ export async function parseJwt(encodedToken, issuer, audience) {
9
11
  catch {
10
12
  return { valid: false, reason: `Unable to decode JWT.` };
11
13
  }
12
- if (typeof decoded.header.typ !== 'undefined' &&
13
- decoded.header.typ !== 'JWT') {
14
+ const { typ, alg } = decoded.header;
15
+ if (typeof typ !== 'undefined' && typ !== 'JWT') {
14
16
  return {
15
17
  valid: false,
16
- reason: `Invalid JWT type "${decoded.header.typ}". Expected "JWT".`
18
+ reason: `Invalid JWT type ${JSON.stringify(typ)}. Expected "JWT".`
17
19
  };
18
20
  }
19
- if (decoded.header.alg !== 'RS256') {
21
+ if (!algToHash[alg]) {
20
22
  return {
21
23
  valid: false,
22
- reason: `Invalid JWT algorithm "${decoded.header.alg}". Expected "RS256".`
24
+ reason: `Invalid JWT algorithm ${JSON.stringify(alg)}. Supported: ${algs}.`
23
25
  };
24
26
  }
25
- if (typeof decoded.payload.aud === 'string' &&
26
- decoded.payload.aud !== audience) {
27
+ const { sub, aud, iss, iat, exp, nbf } = decoded.payload;
28
+ if (typeof sub !== 'string') {
27
29
  return {
28
30
  valid: false,
29
- reason: `Invalid JWT audience "${decoded.payload.aud}". Expected "${audience}".`
31
+ reason: `Subject claim (sub) is required and must be a string. Received ${JSON.stringify(sub)}.`
30
32
  };
31
33
  }
32
- else if (Array.isArray(decoded.payload.aud) &&
33
- !decoded.payload.aud.includes(audience)) {
34
+ if (typeof aud === 'string') {
35
+ if (aud !== audience) {
36
+ return {
37
+ valid: false,
38
+ reason: `Invalid JWT audience claim (aud) ${JSON.stringify(aud)}. Expected "${audience}".`
39
+ };
40
+ }
41
+ }
42
+ else if (Array.isArray(aud) &&
43
+ aud.length > 0 &&
44
+ aud.every(a => typeof a === 'string')) {
45
+ if (!aud.includes(audience)) {
46
+ return {
47
+ valid: false,
48
+ reason: `Invalid JWT audience claim array (aud) ${JSON.stringify(aud)}. Does not include "${audience}".`
49
+ };
50
+ }
51
+ }
52
+ else {
34
53
  return {
35
54
  valid: false,
36
- reason: `Invalid JWT audience in array "${decoded.payload.aud}". Does not include "${audience}".`
55
+ reason: `Invalid JWT audience claim (aud) ${JSON.stringify(aud)}. Expected a string or a non-empty array of strings.`
37
56
  };
38
57
  }
39
- if (decoded.payload.iss !== issuer) {
58
+ if (iss !== issuer) {
40
59
  return {
41
60
  valid: false,
42
- reason: `Invalid JWT issuer "${decoded.payload.iss}". Expected "${issuer}".`
61
+ reason: `Invalid JWT issuer claim (iss) ${JSON.stringify(decoded.payload.iss)}. Expected "${issuer}".`
62
+ };
63
+ }
64
+ if (typeof exp !== 'number') {
65
+ return {
66
+ valid: false,
67
+ reason: `Invalid JWT expiry date claim (exp) ${JSON.stringify(exp)}. Expected number.`
43
68
  };
44
69
  }
45
- const expiryDate = new Date(0);
46
- expiryDate.setUTCSeconds(decoded.payload.exp);
47
70
  const currentDate = new Date(Date.now());
48
- const expired = expiryDate <= currentDate;
71
+ const expiryDate = new Date(0);
72
+ expiryDate.setUTCSeconds(exp);
73
+ const expired = expiryDate.getTime() <= currentDate.getTime() - skewMs;
49
74
  if (expired) {
50
75
  return {
51
76
  valid: false,
52
77
  reason: `JWT is expired. Expiry date: ${expiryDate}; Current date: ${currentDate};`
53
78
  };
54
79
  }
80
+ if (nbf !== undefined) {
81
+ if (typeof nbf !== 'number') {
82
+ return {
83
+ valid: false,
84
+ reason: `Invalid JWT not before date claim (nbf) ${JSON.stringify(nbf)}. Expected number.`
85
+ };
86
+ }
87
+ const notBeforeDate = new Date(0);
88
+ notBeforeDate.setUTCSeconds(nbf);
89
+ const early = notBeforeDate.getTime() > currentDate.getTime() + skewMs;
90
+ if (early) {
91
+ return {
92
+ valid: false,
93
+ reason: `JWT cannot be used prior to not before date claim (nbf). Not before date: ${notBeforeDate}; Current date: ${currentDate};`
94
+ };
95
+ }
96
+ }
97
+ if (iat !== undefined) {
98
+ if (typeof iat !== 'number') {
99
+ return {
100
+ valid: false,
101
+ reason: `Invalid JWT issued at date claim (iat) ${JSON.stringify(iat)}. Expected number.`
102
+ };
103
+ }
104
+ const issuedAtDate = new Date(0);
105
+ issuedAtDate.setUTCSeconds(iat);
106
+ const postIssued = issuedAtDate.getTime() > currentDate.getTime() + skewMs;
107
+ if (postIssued) {
108
+ return {
109
+ valid: false,
110
+ reason: `JWT issued at date claim (iat) is in the future. Issued at date: ${issuedAtDate}; Current date: ${currentDate};`
111
+ };
112
+ }
113
+ }
55
114
  let key;
56
115
  try {
57
- key = await getKey(decoded);
116
+ key = await resolveKey(decoded);
58
117
  }
59
- catch {
118
+ catch (e) {
119
+ return {
120
+ valid: false,
121
+ reason: `Error retrieving public key to verify JWT signature: ${e instanceof Error ? e.message : e}`
122
+ };
123
+ }
124
+ if (!key) {
60
125
  return {
61
126
  valid: false,
62
- reason: `Error retrieving key to verify JWT signature.`
127
+ reason: `Unable to resolve public key to verify JWT signature.`
63
128
  };
64
129
  }
65
130
  let signatureValid;
package/dist/types.d.ts CHANGED
@@ -4,6 +4,8 @@ export interface JwtPayload {
4
4
  aud: string | Array<string>;
5
5
  iat: number;
6
6
  exp: number;
7
+ nbf?: number;
8
+ jti?: string;
7
9
  }
8
10
  export interface JwtHeader {
9
11
  typ?: string;
@@ -18,7 +20,7 @@ export interface RawJwt {
18
20
  export interface DecodedJwt {
19
21
  header: JwtHeader;
20
22
  payload: JwtPayload;
21
- signature: string;
23
+ signature: Uint8Array;
22
24
  raw: RawJwt;
23
25
  }
24
26
  export interface JsonWebKeyset {
@@ -32,4 +34,30 @@ export interface InvalidJwtParseResult {
32
34
  valid: false;
33
35
  reason: string;
34
36
  }
37
+ export interface IssuerMetadata {
38
+ issuer: string;
39
+ authorization_endpoint?: string;
40
+ token_endpoint?: string;
41
+ jwks_uri?: string;
42
+ userinfo_endpoint?: string;
43
+ revocation_endpoint?: string;
44
+ end_session_endpoint?: string;
45
+ registration_endpoint?: string;
46
+ token_endpoint_auth_methods_supported?: string[];
47
+ token_endpoint_auth_signing_alg_values_supported?: string[];
48
+ introspection_endpoint_auth_methods_supported?: string[];
49
+ introspection_endpoint_auth_signing_alg_values_supported?: string[];
50
+ revocation_endpoint_auth_methods_supported?: string[];
51
+ revocation_endpoint_auth_signing_alg_values_supported?: string[];
52
+ request_object_signing_alg_values_supported?: string[];
53
+ mtls_endpoint_aliases?: MtlsEndpointAliases;
54
+ [key: string]: unknown;
55
+ }
56
+ export interface MtlsEndpointAliases {
57
+ token_endpoint?: string;
58
+ userinfo_endpoint?: string;
59
+ revocation_endpoint?: string;
60
+ introspection_endpoint?: string;
61
+ device_authorization_endpoint?: string;
62
+ }
35
63
  export declare type JwtParseResult = ValidJwtParseResult | InvalidJwtParseResult;
package/dist/verify.js CHANGED
@@ -1,6 +1,5 @@
1
1
  export async function verifyJwtSignature(decoded, key) {
2
2
  const encoder = new TextEncoder();
3
3
  const data = encoder.encode(`${decoded.raw.header}.${decoded.raw.payload}`);
4
- const signature = new Uint8Array(Array.from(decoded.signature).map(c => c.charCodeAt(0)));
5
- return crypto.subtle.verify('RSASSA-PKCS1-v1_5', key, signature, data);
4
+ return crypto.subtle.verify('RSASSA-PKCS1-v1_5', key, decoded.signature, data);
6
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cfworker/jwt",
3
- "version": "1.12.2",
3
+ "version": "4.0.0",
4
4
  "description": "Tiny lib for decoding JWTs and verifying signatures, using native crypto APIs",
5
5
  "keywords": [
6
6
  "jwt",
@@ -34,9 +34,9 @@
34
34
  "test": "cfworker test test/**/*.spec.ts --nocheck"
35
35
  },
36
36
  "dependencies": {
37
- "@cfworker/base64url": "^1.12.2",
38
37
  "@cfworker/worker-types": "^1.12.1",
39
- "@cloudflare/workers-types": "^2.2.2"
38
+ "@cloudflare/workers-types": "^2.2.2",
39
+ "rfc4648": "^1.5.1"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@cfworker/dev": "^1.14.0",
package/src/algs.ts ADDED
@@ -0,0 +1,7 @@
1
+ export const algToHash: Record<string, string> = {
2
+ RS256: 'SHA-256',
3
+ RS384: 'SHA-384',
4
+ RS512: 'SHA-512'
5
+ };
6
+
7
+ export const algs = Object.keys(algToHash);
package/src/decode.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { decode } from '@cfworker/base64url';
1
+ import { base64url } from 'rfc4648';
2
2
  import { DecodedJwt } from './types.js';
3
3
 
4
4
  /**
@@ -6,10 +6,15 @@ import { DecodedJwt } from './types.js';
6
6
  */
7
7
  export function decodeJwt(token: string): DecodedJwt {
8
8
  const [header, payload, signature] = token.split('.');
9
+ const decoder = new TextDecoder();
9
10
  return {
10
- header: JSON.parse(decode(header)),
11
- payload: JSON.parse(decode(payload)),
12
- signature: decode(signature),
11
+ header: JSON.parse(
12
+ decoder.decode(base64url.parse(header, { loose: true }))
13
+ ),
14
+ payload: JSON.parse(
15
+ decoder.decode(base64url.parse(payload, { loose: true }))
16
+ ),
17
+ signature: base64url.parse(signature, { loose: true }),
13
18
  raw: { header, payload, signature }
14
19
  };
15
20
  }
@@ -0,0 +1,21 @@
1
+ import { IssuerMetadata } from './types';
2
+
3
+ /**
4
+ * Fetch an oidc discovery document
5
+ */
6
+ export async function getIssuerMetadata(
7
+ issuer: string
8
+ ): Promise<IssuerMetadata> {
9
+ const url = new URL(issuer);
10
+ if (!url.pathname.endsWith('/')) {
11
+ url.pathname += '/';
12
+ }
13
+ url.pathname += '.well-known/openid-configuration';
14
+ const response = await fetch(url.href);
15
+ if (!response.ok) {
16
+ throw new Error(
17
+ `Error loading OpenID discovery document at ${url.href}. ${response.status} ${response.statusText}`
18
+ );
19
+ }
20
+ return response.json();
21
+ }
package/src/jwks.ts CHANGED
@@ -1,14 +1,24 @@
1
+ import { algToHash } from './algs.js';
2
+ import { getIssuerMetadata } from './discovery.js';
1
3
  import { DecodedJwt, JsonWebKeyset } from './types.js';
2
4
 
3
5
  /**
4
6
  * Fetch a json web keyset.
5
7
  */
6
8
  export async function getJwks(issuer: string): Promise<JsonWebKeyset> {
7
- const url = new URL(issuer);
8
- if (!url.pathname.endsWith('/')) {
9
- url.pathname += '/';
9
+ const issuerMetadata = await getIssuerMetadata(issuer);
10
+
11
+ let url;
12
+ if (issuerMetadata.jwks_uri) {
13
+ url = new URL(issuerMetadata.jwks_uri);
14
+ } else {
15
+ url = new URL(issuer);
16
+ if (!url.pathname.endsWith('/')) {
17
+ url.pathname += '/';
18
+ }
19
+ url.pathname += '.well-known/jwks.json';
10
20
  }
11
- url.pathname += '.well-known/jwks.json';
21
+
12
22
  const response = await fetch(url.href);
13
23
  if (!response.ok) {
14
24
  throw new Error(
@@ -26,22 +36,33 @@ const importedKeys: Record<string, Record<string, CryptoKey>> = {};
26
36
  * @param jwks The JsonWebKeyset to import.
27
37
  */
28
38
  export async function importKey(iss: string, jwk: JsonWebKey) {
29
- const input = {
30
- kty: 'RSA',
31
- e: 'AQAB',
32
- n: jwk.n,
33
- alg: 'RS256',
34
- ext: true
35
- };
39
+ if (jwk.kty !== 'RSA') {
40
+ throw new Error(
41
+ `Unsupported jwk key type (kty) "${
42
+ jwk.kty
43
+ }": Full JWK was ${JSON.stringify(jwk)}`
44
+ );
45
+ }
46
+ // alg is not mandatory in a JWK but is available in the JWT, for now we default to
47
+ // SHA-256 (RS256) because this is the most common but evaluating the key length
48
+ // of the JWKS is probably an ideal way of identifying what the alg is.
49
+ const hash = jwk.alg ? algToHash[jwk.alg] : 'SHA-256';
50
+ if (!hash) {
51
+ throw new Error(
52
+ `Unsupported jwk Algorithm (alg) "${
53
+ jwk.alg
54
+ }": Full JWK was ${JSON.stringify(jwk)}`
55
+ );
56
+ }
36
57
  const key = await crypto.subtle.importKey(
37
58
  'jwk',
38
- input,
39
- { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },
59
+ jwk,
60
+ { name: 'RSASSA-PKCS1-v1_5', hash },
40
61
  false,
41
62
  ['verify']
42
63
  );
43
64
  importedKeys[iss] = importedKeys[iss] || {};
44
- importedKeys[iss][jwk.kid || 'default'] = key;
65
+ importedKeys[iss][jwk.kid ?? 'default'] = key;
45
66
  }
46
67
 
47
68
  /**
package/src/parse.ts CHANGED
@@ -1,15 +1,19 @@
1
+ import { algs, algToHash } from './algs.js';
1
2
  import { decodeJwt } from './decode.js';
2
3
  import { getKey } from './jwks.js';
3
4
  import { DecodedJwt, JwtParseResult } from './types.js';
4
5
  import { verifyJwtSignature } from './verify.js';
5
6
 
7
+ const skewMs = 30 * 1000;
8
+
6
9
  /**
7
10
  * Parse a JWT.
8
11
  */
9
12
  export async function parseJwt(
10
13
  encodedToken: string,
11
14
  issuer: string,
12
- audience: string
15
+ audience: string,
16
+ resolveKey: (decoded: DecodedJwt) => Promise<CryptoKey | null> = getKey
13
17
  ): Promise<JwtParseResult> {
14
18
  let decoded: DecodedJwt;
15
19
  try {
@@ -17,62 +21,146 @@ export async function parseJwt(
17
21
  } catch {
18
22
  return { valid: false, reason: `Unable to decode JWT.` };
19
23
  }
20
- if (
21
- typeof decoded.header.typ !== 'undefined' &&
22
- decoded.header.typ !== 'JWT'
23
- ) {
24
+ const { typ, alg } = decoded.header;
25
+ if (typeof typ !== 'undefined' && typ !== 'JWT') {
24
26
  return {
25
27
  valid: false,
26
- reason: `Invalid JWT type "${decoded.header.typ}". Expected "JWT".`
28
+ reason: `Invalid JWT type ${JSON.stringify(typ)}. Expected "JWT".`
27
29
  };
28
30
  }
29
- if (decoded.header.alg !== 'RS256') {
31
+ if (!algToHash[alg]) {
30
32
  return {
31
33
  valid: false,
32
- reason: `Invalid JWT algorithm "${decoded.header.alg}". Expected "RS256".`
34
+ reason: `Invalid JWT algorithm ${JSON.stringify(
35
+ alg
36
+ )}. Supported: ${algs}.`
33
37
  };
34
38
  }
35
- if (
36
- typeof decoded.payload.aud === 'string' &&
37
- decoded.payload.aud !== audience
38
- ) {
39
+
40
+ const { sub, aud, iss, iat, exp, nbf } = decoded.payload;
41
+ if (typeof sub !== 'string') {
39
42
  return {
40
43
  valid: false,
41
- reason: `Invalid JWT audience "${decoded.payload.aud}". Expected "${audience}".`
44
+ reason: `Subject claim (sub) is required and must be a string. Received ${JSON.stringify(
45
+ sub
46
+ )}.`
42
47
  };
48
+ }
49
+
50
+ if (typeof aud === 'string') {
51
+ if (aud !== audience) {
52
+ return {
53
+ valid: false,
54
+ reason: `Invalid JWT audience claim (aud) ${JSON.stringify(
55
+ aud
56
+ )}. Expected "${audience}".`
57
+ };
58
+ }
43
59
  } else if (
44
- Array.isArray(decoded.payload.aud) &&
45
- !decoded.payload.aud.includes(audience)
60
+ Array.isArray(aud) &&
61
+ aud.length > 0 &&
62
+ aud.every(a => typeof a === 'string')
46
63
  ) {
64
+ if (!aud.includes(audience)) {
65
+ return {
66
+ valid: false,
67
+ reason: `Invalid JWT audience claim array (aud) ${JSON.stringify(
68
+ aud
69
+ )}. Does not include "${audience}".`
70
+ };
71
+ }
72
+ } else {
47
73
  return {
48
74
  valid: false,
49
- reason: `Invalid JWT audience in array "${decoded.payload.aud}". Does not include "${audience}".`
75
+ reason: `Invalid JWT audience claim (aud) ${JSON.stringify(
76
+ aud
77
+ )}. Expected a string or a non-empty array of strings.`
50
78
  };
51
79
  }
52
80
 
53
- if (decoded.payload.iss !== issuer) {
81
+ if (iss !== issuer) {
54
82
  return {
55
83
  valid: false,
56
- reason: `Invalid JWT issuer "${decoded.payload.iss}". Expected "${issuer}".`
84
+ reason: `Invalid JWT issuer claim (iss) ${JSON.stringify(
85
+ decoded.payload.iss
86
+ )}. Expected "${issuer}".`
87
+ };
88
+ }
89
+
90
+ if (typeof exp !== 'number') {
91
+ return {
92
+ valid: false,
93
+ reason: `Invalid JWT expiry date claim (exp) ${JSON.stringify(
94
+ exp
95
+ )}. Expected number.`
57
96
  };
58
97
  }
59
- const expiryDate = new Date(0);
60
- expiryDate.setUTCSeconds(decoded.payload.exp);
61
98
  const currentDate = new Date(Date.now());
62
- const expired = expiryDate <= currentDate;
99
+ const expiryDate = new Date(0);
100
+ expiryDate.setUTCSeconds(exp);
101
+ const expired = expiryDate.getTime() <= currentDate.getTime() - skewMs;
63
102
  if (expired) {
64
103
  return {
65
104
  valid: false,
66
105
  reason: `JWT is expired. Expiry date: ${expiryDate}; Current date: ${currentDate};`
67
106
  };
68
107
  }
69
- let key: CryptoKey;
108
+
109
+ if (nbf !== undefined) {
110
+ if (typeof nbf !== 'number') {
111
+ return {
112
+ valid: false,
113
+ reason: `Invalid JWT not before date claim (nbf) ${JSON.stringify(
114
+ nbf
115
+ )}. Expected number.`
116
+ };
117
+ }
118
+ const notBeforeDate = new Date(0);
119
+ notBeforeDate.setUTCSeconds(nbf);
120
+ const early = notBeforeDate.getTime() > currentDate.getTime() + skewMs;
121
+ if (early) {
122
+ return {
123
+ valid: false,
124
+ reason: `JWT cannot be used prior to not before date claim (nbf). Not before date: ${notBeforeDate}; Current date: ${currentDate};`
125
+ };
126
+ }
127
+ }
128
+
129
+ if (iat !== undefined) {
130
+ if (typeof iat !== 'number') {
131
+ return {
132
+ valid: false,
133
+ reason: `Invalid JWT issued at date claim (iat) ${JSON.stringify(
134
+ iat
135
+ )}. Expected number.`
136
+ };
137
+ }
138
+ const issuedAtDate = new Date(0);
139
+ issuedAtDate.setUTCSeconds(iat);
140
+ const postIssued = issuedAtDate.getTime() > currentDate.getTime() + skewMs;
141
+ if (postIssued) {
142
+ return {
143
+ valid: false,
144
+ reason: `JWT issued at date claim (iat) is in the future. Issued at date: ${issuedAtDate}; Current date: ${currentDate};`
145
+ };
146
+ }
147
+ }
148
+
149
+ let key: CryptoKey | null;
70
150
  try {
71
- key = await getKey(decoded);
72
- } catch {
151
+ key = await resolveKey(decoded);
152
+ } catch (e) {
153
+ return {
154
+ valid: false,
155
+ reason: `Error retrieving public key to verify JWT signature: ${
156
+ e instanceof Error ? e.message : e
157
+ }`
158
+ };
159
+ }
160
+ if (!key) {
73
161
  return {
74
162
  valid: false,
75
- reason: `Error retrieving key to verify JWT signature.`
163
+ reason: `Unable to resolve public key to verify JWT signature.`
76
164
  };
77
165
  }
78
166
  let signatureValid: boolean;
package/src/types.ts CHANGED
@@ -4,6 +4,8 @@ export interface JwtPayload {
4
4
  aud: string | Array<string>;
5
5
  iat: number;
6
6
  exp: number;
7
+ nbf?: number;
8
+ jti?: string;
7
9
  }
8
10
 
9
11
  export interface JwtHeader {
@@ -28,7 +30,7 @@ export interface RawJwt {
28
30
  export interface DecodedJwt {
29
31
  header: JwtHeader;
30
32
  payload: JwtPayload;
31
- signature: string;
33
+ signature: Uint8Array;
32
34
  raw: RawJwt;
33
35
  }
34
36
 
@@ -46,4 +48,32 @@ export interface InvalidJwtParseResult {
46
48
  reason: string;
47
49
  }
48
50
 
51
+ export interface IssuerMetadata {
52
+ issuer: string;
53
+ authorization_endpoint?: string;
54
+ token_endpoint?: string;
55
+ jwks_uri?: string;
56
+ userinfo_endpoint?: string;
57
+ revocation_endpoint?: string;
58
+ end_session_endpoint?: string;
59
+ registration_endpoint?: string;
60
+ token_endpoint_auth_methods_supported?: string[];
61
+ token_endpoint_auth_signing_alg_values_supported?: string[];
62
+ introspection_endpoint_auth_methods_supported?: string[];
63
+ introspection_endpoint_auth_signing_alg_values_supported?: string[];
64
+ revocation_endpoint_auth_methods_supported?: string[];
65
+ revocation_endpoint_auth_signing_alg_values_supported?: string[];
66
+ request_object_signing_alg_values_supported?: string[];
67
+ mtls_endpoint_aliases?: MtlsEndpointAliases;
68
+ [key: string]: unknown;
69
+ }
70
+
71
+ export interface MtlsEndpointAliases {
72
+ token_endpoint?: string;
73
+ userinfo_endpoint?: string;
74
+ revocation_endpoint?: string;
75
+ introspection_endpoint?: string;
76
+ device_authorization_endpoint?: string;
77
+ }
78
+
49
79
  export type JwtParseResult = ValidJwtParseResult | InvalidJwtParseResult;
package/src/verify.ts CHANGED
@@ -7,9 +7,11 @@ import { DecodedJwt } from './types.js';
7
7
  export async function verifyJwtSignature(decoded: DecodedJwt, key: CryptoKey) {
8
8
  const encoder = new TextEncoder();
9
9
  const data = encoder.encode(`${decoded.raw.header}.${decoded.raw.payload}`);
10
- const signature = new Uint8Array(
11
- Array.from(decoded.signature).map(c => c.charCodeAt(0))
12
- );
13
10
 
14
- return crypto.subtle.verify('RSASSA-PKCS1-v1_5', key, signature, data);
11
+ return crypto.subtle.verify(
12
+ 'RSASSA-PKCS1-v1_5',
13
+ key,
14
+ decoded.signature,
15
+ data
16
+ );
15
17
  }