@cfworker/jwt 3.0.0 → 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.
@@ -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,10 +1,18 @@
1
1
  import { algToHash } from './algs.js';
2
+ import { getIssuerMetadata } from './discovery.js';
2
3
  export async function getJwks(issuer) {
3
- const url = new URL(issuer);
4
- if (!url.pathname.endsWith('/')) {
5
- 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';
6
15
  }
7
- url.pathname += '.well-known/jwks.json';
8
16
  const response = await fetch(url.href);
9
17
  if (!response.ok) {
10
18
  throw new Error(`Error loading jwks at ${url.href}. ${response.status} ${response.statusText}`);
@@ -14,11 +22,11 @@ export async function getJwks(issuer) {
14
22
  const importedKeys = {};
15
23
  export async function importKey(iss, jwk) {
16
24
  if (jwk.kty !== 'RSA') {
17
- throw new Error(`Unsupported jwk key type (kty) "${jwk.kty}".`);
25
+ throw new Error(`Unsupported jwk key type (kty) "${jwk.kty}": Full JWK was ${JSON.stringify(jwk)}`);
18
26
  }
19
- const hash = jwk.alg ? algToHash[jwk.alg] : null;
27
+ const hash = jwk.alg ? algToHash[jwk.alg] : 'SHA-256';
20
28
  if (!hash) {
21
- throw new Error(`Unsupported jwk Algorithm (alg) "${jwk.alg}".`);
29
+ throw new Error(`Unsupported jwk Algorithm (alg) "${jwk.alg}": Full JWK was ${JSON.stringify(jwk)}`);
22
30
  }
23
31
  const key = await crypto.subtle.importKey('jwk', jwk, { name: 'RSASSA-PKCS1-v1_5', hash }, false, ['verify']);
24
32
  importedKeys[iss] = importedKeys[iss] || {};
package/dist/parse.js CHANGED
@@ -115,10 +115,10 @@ export async function parseJwt(encodedToken, issuer, audience, resolveKey = getK
115
115
  try {
116
116
  key = await resolveKey(decoded);
117
117
  }
118
- catch {
118
+ catch (e) {
119
119
  return {
120
120
  valid: false,
121
- reason: `Error retrieving public key to verify JWT signature.`
121
+ reason: `Error retrieving public key to verify JWT signature: ${e instanceof Error ? e.message : e}`
122
122
  };
123
123
  }
124
124
  if (!key) {
package/dist/types.d.ts CHANGED
@@ -34,4 +34,30 @@ export interface InvalidJwtParseResult {
34
34
  valid: false;
35
35
  reason: string;
36
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
+ }
37
63
  export declare type JwtParseResult = ValidJwtParseResult | InvalidJwtParseResult;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cfworker/jwt",
3
- "version": "3.0.0",
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",
@@ -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,15 +1,24 @@
1
1
  import { algToHash } from './algs.js';
2
+ import { getIssuerMetadata } from './discovery.js';
2
3
  import { DecodedJwt, JsonWebKeyset } from './types.js';
3
4
 
4
5
  /**
5
6
  * Fetch a json web keyset.
6
7
  */
7
8
  export async function getJwks(issuer: string): Promise<JsonWebKeyset> {
8
- const url = new URL(issuer);
9
- if (!url.pathname.endsWith('/')) {
10
- 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';
11
20
  }
12
- url.pathname += '.well-known/jwks.json';
21
+
13
22
  const response = await fetch(url.href);
14
23
  if (!response.ok) {
15
24
  throw new Error(
@@ -28,11 +37,22 @@ const importedKeys: Record<string, Record<string, CryptoKey>> = {};
28
37
  */
29
38
  export async function importKey(iss: string, jwk: JsonWebKey) {
30
39
  if (jwk.kty !== 'RSA') {
31
- throw new Error(`Unsupported jwk key type (kty) "${jwk.kty}".`);
40
+ throw new Error(
41
+ `Unsupported jwk key type (kty) "${
42
+ jwk.kty
43
+ }": Full JWK was ${JSON.stringify(jwk)}`
44
+ );
32
45
  }
33
- const hash = jwk.alg ? algToHash[jwk.alg] : null;
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';
34
50
  if (!hash) {
35
- throw new Error(`Unsupported jwk Algorithm (alg) "${jwk.alg}".`);
51
+ throw new Error(
52
+ `Unsupported jwk Algorithm (alg) "${
53
+ jwk.alg
54
+ }": Full JWK was ${JSON.stringify(jwk)}`
55
+ );
36
56
  }
37
57
  const key = await crypto.subtle.importKey(
38
58
  'jwk',
package/src/parse.ts CHANGED
@@ -149,10 +149,12 @@ export async function parseJwt(
149
149
  let key: CryptoKey | null;
150
150
  try {
151
151
  key = await resolveKey(decoded);
152
- } catch {
152
+ } catch (e) {
153
153
  return {
154
154
  valid: false,
155
- reason: `Error retrieving public key to verify JWT signature.`
155
+ reason: `Error retrieving public key to verify JWT signature: ${
156
+ e instanceof Error ? e.message : e
157
+ }`
156
158
  };
157
159
  }
158
160
  if (!key) {
package/src/types.ts CHANGED
@@ -48,4 +48,32 @@ export interface InvalidJwtParseResult {
48
48
  reason: string;
49
49
  }
50
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
+
51
79
  export type JwtParseResult = ValidJwtParseResult | InvalidJwtParseResult;