oauth4webapi 2.0.6 → 2.2.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
@@ -39,7 +39,7 @@ import * as oauth2 from 'oauth4webapi'
39
39
  **`example`** Deno import
40
40
 
41
41
  ```js
42
- import * as oauth2 from 'https://deno.land/x/oauth4webapi@v2.0.6/mod.ts'
42
+ import * as oauth2 from 'https://deno.land/x/oauth4webapi@v2.2.0/mod.ts'
43
43
  ```
44
44
 
45
45
  - Authorization Code Flow - OpenID Connect [source](examples/code.ts), or plain OAuth 2 [source](examples/oauth.ts)
package/build/index.d.ts CHANGED
@@ -45,31 +45,67 @@ export type ClientAuthenticationMethod = 'client_secret_basic' | 'client_secret_
45
45
  /**
46
46
  * Supported JWS `alg` Algorithm identifiers.
47
47
  *
48
- * @example CryptoKey algorithm for the `PS256` JWS Algorithm Identifier
48
+ * @example CryptoKey algorithm for the `PS256`, `PS384`, or `PS512` JWS Algorithm Identifiers
49
49
  *
50
50
  * ```ts
51
- * interface Ps256Algorithm extends RsaHashedKeyAlgorithm {
51
+ * interface RSAPSSAlgorithm extends RsaHashedKeyAlgorithm {
52
52
  * name: 'RSA-PSS'
53
+ * hash: { name: 'SHA-256' | 'SHA-384' | 'SHA-512' }
54
+ * }
55
+ *
56
+ * interface PS256 extends RSAPSSAlgorithm {
53
57
  * hash: { name: 'SHA-256' }
54
58
  * }
59
+ *
60
+ * interface PS384 extends RSAPSSAlgorithm {
61
+ * hash: { name: 'SHA-384' }
62
+ * }
63
+ *
64
+ * interface PS512 extends RSAPSSAlgorithm {
65
+ * hash: { name: 'SHA-512' }
66
+ * }
55
67
  * ```
56
68
  *
57
- * @example CryptoKey algorithm for the `ES256` JWS Algorithm Identifier
69
+ * @example CryptoKey algorithm for the `ES256`, `ES384`, or `ES512` JWS Algorithm Identifiers
58
70
  *
59
71
  * ```ts
60
- * interface Es256Algorithm extends EcKeyAlgorithm {
72
+ * interface ECDSAAlgorithm extends EcKeyAlgorithm {
61
73
  * name: 'ECDSA'
74
+ * namedCurve: 'P-256' | 'P-384' | 'P-521'
75
+ * }
76
+ *
77
+ * interface ES256 extends ECDSAAlgorithm {
62
78
  * namedCurve: 'P-256'
63
79
  * }
80
+ *
81
+ * interface ES384 extends ECDSAAlgorithm {
82
+ * namedCurve: 'P-384'
83
+ * }
84
+ *
85
+ * interface ES512 extends ECDSAAlgorithm {
86
+ * namedCurve: 'P-521'
87
+ * }
64
88
  * ```
65
89
  *
66
- * @example CryptoKey algorithm for the `RS256` JWS Algorithm Identifier
90
+ * @example CryptoKey algorithm for the `RS256`, `RS384`, or `RS512` JWS Algorithm Identifiers
67
91
  *
68
92
  * ```ts
69
- * interface Rs256Algorithm extends RsaHashedKeyAlgorithm {
93
+ * interface ECDSAAlgorithm extends RsaHashedKeyAlgorithm {
70
94
  * name: 'RSASSA-PKCS1-v1_5'
95
+ * hash: { name: 'SHA-256' | 'SHA-384' | 'SHA-512' }
96
+ * }
97
+ *
98
+ * interface RS256 extends ECDSAAlgorithm {
71
99
  * hash: { name: 'SHA-256' }
72
100
  * }
101
+ *
102
+ * interface RS384 extends ECDSAAlgorithm {
103
+ * hash: { name: 'SHA-384' }
104
+ * }
105
+ *
106
+ * interface RS512 extends ECDSAAlgorithm {
107
+ * hash: { name: 'SHA-512' }
108
+ * }
73
109
  * ```
74
110
  *
75
111
  * @example CryptoKey algorithm for the `EdDSA` JWS Algorithm Identifier (Experimental)
@@ -79,12 +115,16 @@ export type ClientAuthenticationMethod = 'client_secret_basic' | 'client_secret_
79
115
  * widely adopted. If the proposal changes this implementation will follow up with a minor release.
80
116
  *
81
117
  * ```ts
82
- * interface EdDSAAlgorithm extends KeyAlgorithm {
83
- * name: 'Ed25519'
118
+ * interface EdDSA extends KeyAlgorithm {
119
+ * name: 'Ed25519' | 'Ed448'
84
120
  * }
85
121
  * ```
86
122
  */
87
- export type JWSAlgorithm = 'PS256' | 'ES256' | 'RS256' | 'EdDSA';
123
+ export type JWSAlgorithm = 'PS256' | 'ES256' | 'RS256' | 'EdDSA' | 'ES384' | 'PS384' | 'RS384' | 'ES512' | 'PS512' | 'RS512';
124
+ /** @ignore during Documentation generation but part of the public API */
125
+ export declare const clockSkew: unique symbol;
126
+ /** @ignore during Documentation generation but part of the public API */
127
+ export declare const clockTolerance: unique symbol;
88
128
  /**
89
129
  * Authorization Server Metadata
90
130
  *
@@ -406,6 +446,50 @@ export interface Client {
406
446
  introspection_signed_response_alg?: string;
407
447
  /** Default Maximum Authentication Age. */
408
448
  default_max_age?: number;
449
+ /**
450
+ * Use to adjust the client's assumed current time. Positive and negative finite values
451
+ * representing seconds are allowed. Default is `0` (Date.now() + 0 seconds is used).
452
+ *
453
+ * @ignore during Documentation generation but part of the public API
454
+ *
455
+ * @example Client's local clock is mistakenly 1 hour in the past
456
+ *
457
+ * ```ts
458
+ * const client: oauth.Client = {
459
+ * client_id: 'abc4ba37-4ab8-49b5-99d4-9441ba35d428',
460
+ * // ... other metadata
461
+ * [oauth.clockSkew]: +(60 * 60),
462
+ * }
463
+ * ```
464
+ *
465
+ * @example Client's local clock is mistakenly 1 hour in the future
466
+ *
467
+ * ```ts
468
+ * const client: oauth.Client = {
469
+ * client_id: 'abc4ba37-4ab8-49b5-99d4-9441ba35d428',
470
+ * // ... other metadata
471
+ * [oauth.clockSkew]: -(60 * 60),
472
+ * }
473
+ * ```
474
+ */
475
+ [clockSkew]?: number;
476
+ /**
477
+ * Use to set allowed client's clock tolerance when checking DateTime JWT Claims. Only positive
478
+ * finite values representing seconds are allowed. Default is `30` (30 seconds).
479
+ *
480
+ * @ignore during Documentation generation but part of the public API
481
+ *
482
+ * @example Tolerate 30 seconds clock skew when validating JWT claims like `exp` or `nbf`.
483
+ *
484
+ * ```ts
485
+ * const client: oauth.Client = {
486
+ * client_id: 'abc4ba37-4ab8-49b5-99d4-9441ba35d428',
487
+ * // ... other metadata
488
+ * [oauth.clockTolerance]: 30,
489
+ * }
490
+ * ```
491
+ */
492
+ [clockTolerance]?: number;
409
493
  [metadata: string]: JsonValue | undefined;
410
494
  }
411
495
  export declare class UnsupportedOperationError extends Error {
@@ -593,6 +677,16 @@ export declare function parseWwwAuthenticateChallenges(response: Response): WWWA
593
677
  */
594
678
  export declare function processPushedAuthorizationResponse(as: AuthorizationServer, client: Client, response: Response): Promise<PushedAuthorizationResponse | OAuth2Error>;
595
679
  export interface ProtectedResourceRequestOptions extends Omit<HttpRequestOptions, 'headers'>, DPoPRequestOptions {
680
+ /**
681
+ * Use to adjust the client's assumed current time. Positive and negative finite values
682
+ * representing seconds are allowed. Default is `0` (Date.now() + 0 seconds is used).
683
+ *
684
+ * This option only affects the request if the {@link ProtectedResourceRequestOptions.DPoP DPoP}
685
+ * option is also used.
686
+ *
687
+ * @ignore during Documentation generation but part of the public API
688
+ */
689
+ clockSkew?: number;
596
690
  }
597
691
  /**
598
692
  * Performs a protected resource request at an arbitrary URL.
@@ -1082,6 +1176,8 @@ export interface GenerateKeyPairOptions {
1082
1176
  extractable?: boolean;
1083
1177
  /** (RSA algorithms only) The length, in bits, of the RSA modulus. Default is `2048`. */
1084
1178
  modulusLength?: number;
1179
+ /** (EdDSA algorithms only) The EdDSA sub-type. Default is `Ed25519`. */
1180
+ crv?: 'Ed25519' | 'Ed448';
1085
1181
  }
1086
1182
  /**
1087
1183
  * Generates a CryptoKeyPair for a given JWS `alg` Algorithm identifier.
package/build/index.js CHANGED
@@ -1,9 +1,11 @@
1
1
  let USER_AGENT;
2
2
  if (typeof navigator === 'undefined' || !navigator.userAgent?.startsWith?.('Mozilla/5.0 ')) {
3
3
  const NAME = 'oauth4webapi';
4
- const VERSION = 'v2.0.6';
4
+ const VERSION = 'v2.2.0';
5
5
  USER_AGENT = `${NAME}/${VERSION}`;
6
6
  }
7
+ export const clockSkew = Symbol();
8
+ export const clockTolerance = Symbol();
7
9
  const encoder = new TextEncoder();
8
10
  const decoder = new TextDecoder();
9
11
  function buf(input) {
@@ -113,7 +115,18 @@ function isPrivateKey(key) {
113
115
  function isPublicKey(key) {
114
116
  return isCryptoKey(key) && key.type === 'public';
115
117
  }
116
- const SUPPORTED_JWS_ALGS = ['PS256', 'ES256', 'RS256', 'EdDSA'];
118
+ const SUPPORTED_JWS_ALGS = [
119
+ 'PS256',
120
+ 'ES256',
121
+ 'RS256',
122
+ 'PS384',
123
+ 'ES384',
124
+ 'RS384',
125
+ 'PS512',
126
+ 'ES512',
127
+ 'RS512',
128
+ 'EdDSA',
129
+ ];
117
130
  function processDpopNonce(response) {
118
131
  const url = new URL(response.url);
119
132
  if (response.headers.has('dpop-nonce')) {
@@ -263,6 +276,10 @@ function psAlg(key) {
263
276
  switch (key.algorithm.hash.name) {
264
277
  case 'SHA-256':
265
278
  return 'PS256';
279
+ case 'SHA-384':
280
+ return 'PS384';
281
+ case 'SHA-512':
282
+ return 'PS512';
266
283
  default:
267
284
  throw new UnsupportedOperationError('unsupported RsaHashedKeyAlgorithm hash name');
268
285
  }
@@ -271,6 +288,10 @@ function rsAlg(key) {
271
288
  switch (key.algorithm.hash.name) {
272
289
  case 'SHA-256':
273
290
  return 'RS256';
291
+ case 'SHA-384':
292
+ return 'RS384';
293
+ case 'SHA-512':
294
+ return 'RS512';
274
295
  default:
275
296
  throw new UnsupportedOperationError('unsupported RsaHashedKeyAlgorithm hash name');
276
297
  }
@@ -279,11 +300,15 @@ function esAlg(key) {
279
300
  switch (key.algorithm.namedCurve) {
280
301
  case 'P-256':
281
302
  return 'ES256';
303
+ case 'P-384':
304
+ return 'ES384';
305
+ case 'P-521':
306
+ return 'ES512';
282
307
  default:
283
308
  throw new UnsupportedOperationError('unsupported EcKeyAlgorithm namedCurve');
284
309
  }
285
310
  }
286
- function determineJWSAlgorithm(key) {
311
+ function keyToJws(key) {
287
312
  switch (key.algorithm.name) {
288
313
  case 'RSA-PSS':
289
314
  return psAlg(key);
@@ -292,16 +317,30 @@ function determineJWSAlgorithm(key) {
292
317
  case 'ECDSA':
293
318
  return esAlg(key);
294
319
  case 'Ed25519':
320
+ case 'Ed448':
295
321
  return 'EdDSA';
296
322
  default:
297
323
  throw new UnsupportedOperationError('unsupported CryptoKey algorithm name');
298
324
  }
299
325
  }
326
+ function getClockSkew(client) {
327
+ if (Number.isFinite(client[clockSkew])) {
328
+ return client[clockSkew];
329
+ }
330
+ return 0;
331
+ }
332
+ function getClockTolerance(client) {
333
+ const tolerance = client[clockTolerance];
334
+ if (Number.isFinite(tolerance) && Math.sign(tolerance) !== -1) {
335
+ return tolerance;
336
+ }
337
+ return 30;
338
+ }
300
339
  function epochTime() {
301
340
  return Math.floor(Date.now() / 1000);
302
341
  }
303
342
  function clientAssertion(as, client) {
304
- const now = epochTime();
343
+ const now = epochTime() + getClockSkew(client);
305
344
  return {
306
345
  jti: randomBytes(),
307
346
  aud: [as.issuer, as.token_endpoint],
@@ -314,7 +353,7 @@ function clientAssertion(as, client) {
314
353
  }
315
354
  async function privateKeyJwt(as, client, key, kid) {
316
355
  return jwt({
317
- alg: determineJWSAlgorithm(key),
356
+ alg: keyToJws(key),
318
357
  kid,
319
358
  }, clientAssertion(as, client), key);
320
359
  }
@@ -398,7 +437,7 @@ async function jwt(header, claimsSet, key) {
398
437
  throw new TypeError('CryptoKey instances used for signing assertions must include "sign" in their "usages"');
399
438
  }
400
439
  const input = `${b64u(buf(JSON.stringify(header)))}.${b64u(buf(JSON.stringify(claimsSet)))}`;
401
- const signature = b64u(await crypto.subtle.sign(subtleAlgorithm(key), key, buf(input)));
440
+ const signature = b64u(await crypto.subtle.sign(keyToSubtle(key), key, buf(input)));
402
441
  return `${input}.${signature}`;
403
442
  }
404
443
  export async function issueRequestObject(as, client, parameters, privateKey) {
@@ -413,7 +452,7 @@ export async function issueRequestObject(as, client, parameters, privateKey) {
413
452
  throw new TypeError('"privateKey.key" must be a private CryptoKey');
414
453
  }
415
454
  parameters.set('client_id', client.client_id);
416
- const now = epochTime();
455
+ const now = epochTime() + getClockSkew(client);
417
456
  const claims = {
418
457
  ...Object.fromEntries(parameters.entries()),
419
458
  jti: randomBytes(),
@@ -445,12 +484,12 @@ export async function issueRequestObject(as, client, parameters, privateKey) {
445
484
  }
446
485
  }
447
486
  return jwt({
448
- alg: determineJWSAlgorithm(key),
487
+ alg: keyToJws(key),
449
488
  typ: 'oauth-authz-req+jwt',
450
489
  kid,
451
490
  }, claims, key);
452
491
  }
453
- async function dpopProofJwt(headers, options, url, htm, accessToken) {
492
+ async function dpopProofJwt(headers, options, url, htm, clockSkew, accessToken) {
454
493
  const { privateKey, publicKey, nonce = dpopNonces.get(url.origin) } = options;
455
494
  if (!isPrivateKey(privateKey)) {
456
495
  throw new TypeError('"DPoP.privateKey" must be a private CryptoKey');
@@ -464,9 +503,9 @@ async function dpopProofJwt(headers, options, url, htm, accessToken) {
464
503
  if (!publicKey.extractable) {
465
504
  throw new TypeError('"DPoP.publicKey.extractable" must be true');
466
505
  }
467
- const now = epochTime();
506
+ const now = epochTime() + clockSkew;
468
507
  const proof = await jwt({
469
- alg: determineJWSAlgorithm(privateKey),
508
+ alg: keyToJws(privateKey),
470
509
  typ: 'dpop+jwt',
471
510
  jwk: await publicJwk(publicKey),
472
511
  }, {
@@ -507,7 +546,7 @@ export async function pushedAuthorizationRequest(as, client, parameters, options
507
546
  const headers = prepareHeaders(options?.headers);
508
547
  headers.set('accept', 'application/json');
509
548
  if (options?.DPoP !== undefined) {
510
- await dpopProofJwt(headers, options.DPoP, url, 'POST');
549
+ await dpopProofJwt(headers, options.DPoP, url, 'POST', getClockSkew(client));
511
550
  }
512
551
  return authenticatedRequest(as, client, 'POST', url, body, headers, options);
513
552
  }
@@ -620,7 +659,7 @@ export async function protectedResourceRequest(accessToken, method, url, headers
620
659
  headers.set('authorization', `Bearer ${accessToken}`);
621
660
  }
622
661
  else {
623
- await dpopProofJwt(headers, options.DPoP, url, 'GET', accessToken);
662
+ await dpopProofJwt(headers, options.DPoP, url, 'GET', getClockSkew({ [clockSkew]: options?.clockSkew }), accessToken);
624
663
  headers.set('authorization', `DPoP ${accessToken}`);
625
664
  }
626
665
  return fetch(url.href, {
@@ -646,7 +685,10 @@ export async function userInfoRequest(as, client, accessToken, options) {
646
685
  headers.set('accept', 'application/json');
647
686
  headers.append('accept', 'application/jwt');
648
687
  }
649
- return protectedResourceRequest(accessToken, 'GET', url, headers, null, options);
688
+ return protectedResourceRequest(accessToken, 'GET', url, headers, null, {
689
+ ...options,
690
+ clockSkew: getClockSkew(client),
691
+ });
650
692
  }
651
693
  let jwksCache;
652
694
  async function getPublicSigKeyFromIssuerJwksUri(as, options, header) {
@@ -707,7 +749,9 @@ async function getPublicSigKeyFromIssuerJwksUri(as, options, header) {
707
749
  }
708
750
  switch (true) {
709
751
  case alg === 'ES256' && jwk.crv !== 'P-256':
710
- case alg === 'EdDSA' && jwk.crv !== 'Ed25519':
752
+ case alg === 'ES384' && jwk.crv !== 'P-384':
753
+ case alg === 'ES512' && jwk.crv !== 'P-521':
754
+ case alg === 'EdDSA' && !(jwk.crv === 'Ed25519' || jwk.crv === 'Ed448'):
711
755
  return false;
712
756
  }
713
757
  return true;
@@ -745,7 +789,7 @@ export async function processUserInfoResponse(as, client, expectedSubject, respo
745
789
  let json;
746
790
  if (getContentType(response) === 'application/jwt') {
747
791
  assertReadableResponse(response);
748
- const { claims } = await validateJwt(await response.text(), checkSigningAlgorithm.bind(undefined, client.userinfo_signed_response_alg, as.userinfo_signing_alg_values_supported), noSignatureCheck)
792
+ const { claims } = await validateJwt(await response.text(), checkSigningAlgorithm.bind(undefined, client.userinfo_signed_response_alg, as.userinfo_signing_alg_values_supported), noSignatureCheck, getClockSkew(client), getClockTolerance(client))
749
793
  .then(validateOptionalAudience.bind(undefined, client.client_id))
750
794
  .then(validateOptionalIssuer.bind(undefined, as.issuer));
751
795
  json = claims;
@@ -801,7 +845,7 @@ async function tokenEndpointRequest(as, client, grantType, parameters, options)
801
845
  const headers = prepareHeaders(options?.headers);
802
846
  headers.set('accept', 'application/json');
803
847
  if (options?.DPoP !== undefined) {
804
- await dpopProofJwt(headers, options.DPoP, url, 'POST');
848
+ await dpopProofJwt(headers, options.DPoP, url, 'POST', getClockSkew(client));
805
849
  }
806
850
  return authenticatedRequest(as, client, 'POST', url, parameters, headers, options);
807
851
  }
@@ -873,7 +917,7 @@ async function processGenericAccessTokenResponse(as, client, response, ignoreIdT
873
917
  throw new OPE('"response" body "id_token" property must be a non-empty string');
874
918
  }
875
919
  if (json.id_token) {
876
- const { claims } = await validateJwt(json.id_token, checkSigningAlgorithm.bind(undefined, client.id_token_signed_response_alg, as.id_token_signing_alg_values_supported), noSignatureCheck)
920
+ const { claims } = await validateJwt(json.id_token, checkSigningAlgorithm.bind(undefined, client.id_token_signed_response_alg, as.id_token_signing_alg_values_supported), noSignatureCheck, getClockSkew(client), getClockTolerance(client))
877
921
  .then(validatePresence.bind(undefined, ['aud', 'exp', 'iat', 'iss', 'sub']))
878
922
  .then(validateIssuer.bind(undefined, as.issuer))
879
923
  .then(validateAudience.bind(undefined, client.client_id));
@@ -977,8 +1021,8 @@ export async function processAuthorizationCodeOpenIDResponse(as, client, respons
977
1021
  if (typeof maxAge !== 'number' || maxAge < 0) {
978
1022
  throw new TypeError('"options.max_age" must be a non-negative number');
979
1023
  }
980
- const now = epochTime();
981
- const tolerance = 30;
1024
+ const now = epochTime() + getClockSkew(client);
1025
+ const tolerance = getClockTolerance(client);
982
1026
  if (claims.auth_time + maxAge < now - tolerance) {
983
1027
  throw new OPE('too much time has elapsed since the last End-User authentication');
984
1028
  }
@@ -1105,7 +1149,7 @@ export async function processIntrospectionResponse(as, client, response) {
1105
1149
  let json;
1106
1150
  if (getContentType(response) === 'application/token-introspection+jwt') {
1107
1151
  assertReadableResponse(response);
1108
- const { claims } = await validateJwt(await response.text(), checkSigningAlgorithm.bind(undefined, client.introspection_signed_response_alg, as.introspection_signing_alg_values_supported), noSignatureCheck)
1152
+ const { claims } = await validateJwt(await response.text(), checkSigningAlgorithm.bind(undefined, client.introspection_signed_response_alg, as.introspection_signing_alg_values_supported), noSignatureCheck, getClockSkew(client), getClockTolerance(client))
1109
1153
  .then(checkJwtType.bind(undefined, 'token-introspection+jwt'))
1110
1154
  .then(validatePresence.bind(undefined, ['aud', 'iat', 'iss']))
1111
1155
  .then(validateIssuer.bind(undefined, as.issuer))
@@ -1210,26 +1254,50 @@ function checkRsaKeyAlgorithm(algorithm) {
1210
1254
  throw new OPE(`${algorithm.name} modulusLength must be at least 2048 bits`);
1211
1255
  }
1212
1256
  }
1213
- function subtleAlgorithm(key) {
1257
+ function ecdsaHashName(namedCurve) {
1258
+ switch (namedCurve) {
1259
+ case 'P-256':
1260
+ return 'SHA-256';
1261
+ case 'P-384':
1262
+ return 'SHA-384';
1263
+ case 'P-521':
1264
+ return 'SHA-512';
1265
+ default:
1266
+ throw new UnsupportedOperationError();
1267
+ }
1268
+ }
1269
+ function keyToSubtle(key) {
1214
1270
  switch (key.algorithm.name) {
1215
1271
  case 'ECDSA':
1216
- return { name: key.algorithm.name, hash: { name: 'SHA-256' } };
1217
- case 'RSA-PSS':
1218
- checkRsaKeyAlgorithm(key.algorithm);
1219
1272
  return {
1220
1273
  name: key.algorithm.name,
1221
- saltLength: 256 >> 3,
1274
+ hash: { name: ecdsaHashName(key.algorithm.namedCurve) },
1222
1275
  };
1276
+ case 'RSA-PSS': {
1277
+ checkRsaKeyAlgorithm(key.algorithm);
1278
+ switch (key.algorithm.hash.name) {
1279
+ case 'SHA-256':
1280
+ case 'SHA-384':
1281
+ case 'SHA-512':
1282
+ return {
1283
+ name: key.algorithm.name,
1284
+ saltLength: parseInt(key.algorithm.hash.name.slice(-3), 10) >> 3,
1285
+ };
1286
+ default:
1287
+ throw new UnsupportedOperationError();
1288
+ }
1289
+ }
1223
1290
  case 'RSASSA-PKCS1-v1_5':
1224
1291
  checkRsaKeyAlgorithm(key.algorithm);
1225
1292
  return { name: key.algorithm.name };
1293
+ case 'Ed448':
1226
1294
  case 'Ed25519':
1227
1295
  return { name: key.algorithm.name };
1228
1296
  }
1229
1297
  throw new UnsupportedOperationError();
1230
1298
  }
1231
1299
  const noSignatureCheck = Symbol();
1232
- async function validateJwt(jws, checkAlg, getKey) {
1300
+ async function validateJwt(jws, checkAlg, getKey, clockSkew, clockTolerance) {
1233
1301
  const { 0: protectedHeader, 1: payload, 2: encodedSignature, length } = jws.split('.');
1234
1302
  if (length === 5) {
1235
1303
  throw new UnsupportedOperationError('JWE structure JWTs are not supported');
@@ -1255,7 +1323,7 @@ async function validateJwt(jws, checkAlg, getKey) {
1255
1323
  if (getKey !== noSignatureCheck) {
1256
1324
  const key = await getKey(header);
1257
1325
  const input = `${protectedHeader}.${payload}`;
1258
- const verified = await crypto.subtle.verify(subtleAlgorithm(key), key, signature, buf(input));
1326
+ const verified = await crypto.subtle.verify(keyToSubtle(key), key, signature, buf(input));
1259
1327
  if (!verified) {
1260
1328
  throw new OPE('JWT signature verification failed');
1261
1329
  }
@@ -1270,13 +1338,12 @@ async function validateJwt(jws, checkAlg, getKey) {
1270
1338
  if (!isJsonObject(claims)) {
1271
1339
  throw new OPE('JWT Payload must be a top level object');
1272
1340
  }
1273
- const now = epochTime();
1274
- const tolerance = 30;
1341
+ const now = epochTime() + clockSkew;
1275
1342
  if (claims.exp !== undefined) {
1276
1343
  if (typeof claims.exp !== 'number') {
1277
1344
  throw new OPE('unexpected JWT "exp" (expiration time) claim type');
1278
1345
  }
1279
- if (claims.exp <= now - tolerance) {
1346
+ if (claims.exp <= now - clockTolerance) {
1280
1347
  throw new OPE('unexpected JWT "exp" (expiration time) claim value, timestamp is <= now()');
1281
1348
  }
1282
1349
  }
@@ -1294,7 +1361,7 @@ async function validateJwt(jws, checkAlg, getKey) {
1294
1361
  if (typeof claims.nbf !== 'number') {
1295
1362
  throw new OPE('unexpected JWT "nbf" (not before) claim type');
1296
1363
  }
1297
- if (claims.nbf > now + tolerance) {
1364
+ if (claims.nbf > now + clockTolerance) {
1298
1365
  throw new OPE('unexpected JWT "nbf" (not before) claim value, timestamp is > now()');
1299
1366
  }
1300
1367
  }
@@ -1321,7 +1388,7 @@ export async function validateJwtAuthResponse(as, client, parameters, expectedSt
1321
1388
  if (typeof as.jwks_uri !== 'string') {
1322
1389
  throw new TypeError('"as.jwks_uri" must be a string');
1323
1390
  }
1324
- const { claims } = await validateJwt(response, checkSigningAlgorithm.bind(undefined, client.authorization_signed_response_alg, as.authorization_signing_alg_values_supported), getPublicSigKeyFromIssuerJwksUri.bind(undefined, as, options))
1391
+ const { claims } = await validateJwt(response, checkSigningAlgorithm.bind(undefined, client.authorization_signed_response_alg, as.authorization_signing_alg_values_supported), getPublicSigKeyFromIssuerJwksUri.bind(undefined, as, options), getClockSkew(client), getClockTolerance(client))
1325
1392
  .then(validatePresence.bind(undefined, ['aud', 'exp', 'iss']))
1326
1393
  .then(validateIssuer.bind(undefined, as.issuer))
1327
1394
  .then(validateAudience.bind(undefined, client.client_id));
@@ -1416,26 +1483,38 @@ export function validateAuthResponse(as, client, parameters, expectedState) {
1416
1483
  }
1417
1484
  return new CallbackParameters(parameters);
1418
1485
  }
1419
- async function importJwk(alg, jwk) {
1420
- const { ext, key_ops, use, ...key } = jwk;
1421
- let algorithm;
1486
+ function algToSubtle(alg, crv) {
1422
1487
  switch (alg) {
1423
1488
  case 'PS256':
1424
- algorithm = { name: 'RSA-PSS', hash: { name: 'SHA-256' } };
1425
- break;
1489
+ case 'PS384':
1490
+ case 'PS512':
1491
+ return { name: 'RSA-PSS', hash: { name: `SHA-${alg.slice(-3)}` } };
1426
1492
  case 'RS256':
1427
- algorithm = { name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-256' } };
1428
- break;
1493
+ case 'RS384':
1494
+ case 'RS512':
1495
+ return { name: 'RSASSA-PKCS1-v1_5', hash: { name: `SHA-${alg.slice(-3)}` } };
1429
1496
  case 'ES256':
1430
- algorithm = { name: 'ECDSA', namedCurve: 'P-256' };
1431
- break;
1432
- case 'EdDSA':
1433
- algorithm = { name: 'Ed25519' };
1434
- break;
1497
+ case 'ES384':
1498
+ return { name: 'ECDSA', namedCurve: `P-${alg.slice(-3)}` };
1499
+ case 'ES512':
1500
+ return { name: 'ECDSA', namedCurve: 'P-521' };
1501
+ case 'EdDSA': {
1502
+ switch (crv) {
1503
+ case 'Ed25519':
1504
+ return { name: 'Ed25519' };
1505
+ case 'Ed448':
1506
+ return { name: 'Ed448' };
1507
+ default:
1508
+ throw new UnsupportedOperationError();
1509
+ }
1510
+ }
1435
1511
  default:
1436
1512
  throw new UnsupportedOperationError();
1437
1513
  }
1438
- return crypto.subtle.importKey('jwk', key, algorithm, true, ['verify']);
1514
+ }
1515
+ async function importJwk(alg, jwk) {
1516
+ const { ext, key_ops, use, ...key } = jwk;
1517
+ return crypto.subtle.importKey('jwk', key, algToSubtle(alg, jwk.crv), true, ['verify']);
1439
1518
  }
1440
1519
  export async function deviceAuthorizationRequest(as, client, parameters, options) {
1441
1520
  assertAs(as);
@@ -1512,35 +1591,15 @@ export async function processDeviceCodeResponse(as, client, response) {
1512
1591
  return processGenericAccessTokenResponse(as, client, response);
1513
1592
  }
1514
1593
  export async function generateKeyPair(alg, options) {
1515
- let algorithm;
1516
1594
  if (!validateString(alg)) {
1517
1595
  throw new TypeError('"alg" must be a non-empty string');
1518
1596
  }
1519
- switch (alg) {
1520
- case 'PS256':
1521
- algorithm = {
1522
- name: 'RSA-PSS',
1523
- hash: { name: 'SHA-256' },
1524
- modulusLength: options?.modulusLength ?? 2048,
1525
- publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
1526
- };
1527
- break;
1528
- case 'RS256':
1529
- algorithm = {
1530
- name: 'RSASSA-PKCS1-v1_5',
1531
- hash: { name: 'SHA-256' },
1532
- modulusLength: options?.modulusLength ?? 2048,
1533
- publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
1534
- };
1535
- break;
1536
- case 'ES256':
1537
- algorithm = { name: 'ECDSA', namedCurve: 'P-256' };
1538
- break;
1539
- case 'EdDSA':
1540
- algorithm = { name: 'Ed25519' };
1541
- break;
1542
- default:
1543
- throw new UnsupportedOperationError();
1597
+ const algorithm = algToSubtle(alg, alg === 'EdDSA' ? options?.crv ?? 'Ed25519' : undefined);
1598
+ if (alg.startsWith('PS') || alg.startsWith('RS')) {
1599
+ Object.assign(algorithm, {
1600
+ modulusLength: options?.modulusLength ?? 2048,
1601
+ publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
1602
+ });
1544
1603
  }
1545
1604
  return (crypto.subtle.generateKey(algorithm, options?.extractable ?? false, ['sign', 'verify']));
1546
1605
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oauth4webapi",
3
- "version": "2.0.6",
3
+ "version": "2.2.0",
4
4
  "description": "OAuth 2 / OpenID Connect for Web Platform API JavaScript runtimes",
5
5
  "keywords": [
6
6
  "auth",
@@ -62,21 +62,21 @@
62
62
  "test": "bash -c 'source .node_flags.sh && ava'"
63
63
  },
64
64
  "devDependencies": {
65
- "@esbuild-kit/esm-loader": "^2.5.1",
66
- "@types/node": "^18.11.9",
67
- "@types/qunit": "^2.19.3",
68
- "ava": "^5.1.0",
69
- "edge-runtime": "^2.0.2",
70
- "esbuild": "^0.16.1",
71
- "jose": "^4.11.1",
72
- "patch-package": "^6.5.0",
73
- "prettier": "^2.8.0",
65
+ "@esbuild-kit/esm-loader": "^2.5.5",
66
+ "@types/node": "^18.15.0",
67
+ "@types/qunit": "^2.19.4",
68
+ "ava": "^5.2.0",
69
+ "edge-runtime": "^2.1.2",
70
+ "esbuild": "^0.17.11",
71
+ "jose": "^4.13.1",
72
+ "patch-package": "^6.5.1",
73
+ "prettier": "^2.8.4",
74
74
  "prettier-plugin-jsdoc": "^0.4.2",
75
- "qunit": "^2.19.3",
75
+ "qunit": "^2.19.4",
76
76
  "timekeeper": "^2.2.0",
77
- "typedoc": "^0.23.21",
78
- "typedoc-plugin-markdown": "^3.13.6",
79
- "typescript": "^4.9.3",
80
- "undici": "^5.13.0"
77
+ "typedoc": "^0.23.26",
78
+ "typedoc-plugin-markdown": "^3.14.0",
79
+ "typescript": "^4.9.5",
80
+ "undici": "^5.20.0"
81
81
  }
82
82
  }