oauth4webapi 2.1.0 → 2.2.1

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.1.0/mod.ts'
42
+ import * as oauth2 from 'https://deno.land/x/oauth4webapi@v2.2.1/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
@@ -121,6 +121,10 @@ export type ClientAuthenticationMethod = 'client_secret_basic' | 'client_secret_
121
121
  * ```
122
122
  */
123
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;
124
128
  /**
125
129
  * Authorization Server Metadata
126
130
  *
@@ -442,6 +446,50 @@ export interface Client {
442
446
  introspection_signed_response_alg?: string;
443
447
  /** Default Maximum Authentication Age. */
444
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;
445
493
  [metadata: string]: JsonValue | undefined;
446
494
  }
447
495
  export declare class UnsupportedOperationError extends Error {
@@ -629,6 +677,16 @@ export declare function parseWwwAuthenticateChallenges(response: Response): WWWA
629
677
  */
630
678
  export declare function processPushedAuthorizationResponse(as: AuthorizationServer, client: Client, response: Response): Promise<PushedAuthorizationResponse | OAuth2Error>;
631
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;
632
690
  }
633
691
  /**
634
692
  * Performs a protected resource request at an arbitrary URL.
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.1.0';
4
+ const VERSION = 'v2.2.1';
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) {
@@ -321,11 +323,24 @@ function keyToJws(key) {
321
323
  throw new UnsupportedOperationError('unsupported CryptoKey algorithm name');
322
324
  }
323
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
+ }
324
339
  function epochTime() {
325
340
  return Math.floor(Date.now() / 1000);
326
341
  }
327
342
  function clientAssertion(as, client) {
328
- const now = epochTime();
343
+ const now = epochTime() + getClockSkew(client);
329
344
  return {
330
345
  jti: randomBytes(),
331
346
  aud: [as.issuer, as.token_endpoint],
@@ -437,7 +452,7 @@ export async function issueRequestObject(as, client, parameters, privateKey) {
437
452
  throw new TypeError('"privateKey.key" must be a private CryptoKey');
438
453
  }
439
454
  parameters.set('client_id', client.client_id);
440
- const now = epochTime();
455
+ const now = epochTime() + getClockSkew(client);
441
456
  const claims = {
442
457
  ...Object.fromEntries(parameters.entries()),
443
458
  jti: randomBytes(),
@@ -474,7 +489,7 @@ export async function issueRequestObject(as, client, parameters, privateKey) {
474
489
  kid,
475
490
  }, claims, key);
476
491
  }
477
- async function dpopProofJwt(headers, options, url, htm, accessToken) {
492
+ async function dpopProofJwt(headers, options, url, htm, clockSkew, accessToken) {
478
493
  const { privateKey, publicKey, nonce = dpopNonces.get(url.origin) } = options;
479
494
  if (!isPrivateKey(privateKey)) {
480
495
  throw new TypeError('"DPoP.privateKey" must be a private CryptoKey');
@@ -488,7 +503,7 @@ async function dpopProofJwt(headers, options, url, htm, accessToken) {
488
503
  if (!publicKey.extractable) {
489
504
  throw new TypeError('"DPoP.publicKey.extractable" must be true');
490
505
  }
491
- const now = epochTime();
506
+ const now = epochTime() + clockSkew;
492
507
  const proof = await jwt({
493
508
  alg: keyToJws(privateKey),
494
509
  typ: 'dpop+jwt',
@@ -531,7 +546,7 @@ export async function pushedAuthorizationRequest(as, client, parameters, options
531
546
  const headers = prepareHeaders(options?.headers);
532
547
  headers.set('accept', 'application/json');
533
548
  if (options?.DPoP !== undefined) {
534
- await dpopProofJwt(headers, options.DPoP, url, 'POST');
549
+ await dpopProofJwt(headers, options.DPoP, url, 'POST', getClockSkew(client));
535
550
  }
536
551
  return authenticatedRequest(as, client, 'POST', url, body, headers, options);
537
552
  }
@@ -644,7 +659,7 @@ export async function protectedResourceRequest(accessToken, method, url, headers
644
659
  headers.set('authorization', `Bearer ${accessToken}`);
645
660
  }
646
661
  else {
647
- await dpopProofJwt(headers, options.DPoP, url, 'GET', accessToken);
662
+ await dpopProofJwt(headers, options.DPoP, url, 'GET', getClockSkew({ [clockSkew]: options?.clockSkew }), accessToken);
648
663
  headers.set('authorization', `DPoP ${accessToken}`);
649
664
  }
650
665
  return fetch(url.href, {
@@ -670,7 +685,10 @@ export async function userInfoRequest(as, client, accessToken, options) {
670
685
  headers.set('accept', 'application/json');
671
686
  headers.append('accept', 'application/jwt');
672
687
  }
673
- return protectedResourceRequest(accessToken, 'GET', url, headers, null, options);
688
+ return protectedResourceRequest(accessToken, 'GET', url, headers, null, {
689
+ ...options,
690
+ clockSkew: getClockSkew(client),
691
+ });
674
692
  }
675
693
  let jwksCache;
676
694
  async function getPublicSigKeyFromIssuerJwksUri(as, options, header) {
@@ -771,7 +789,7 @@ export async function processUserInfoResponse(as, client, expectedSubject, respo
771
789
  let json;
772
790
  if (getContentType(response) === 'application/jwt') {
773
791
  assertReadableResponse(response);
774
- 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))
775
793
  .then(validateOptionalAudience.bind(undefined, client.client_id))
776
794
  .then(validateOptionalIssuer.bind(undefined, as.issuer));
777
795
  json = claims;
@@ -827,7 +845,7 @@ async function tokenEndpointRequest(as, client, grantType, parameters, options)
827
845
  const headers = prepareHeaders(options?.headers);
828
846
  headers.set('accept', 'application/json');
829
847
  if (options?.DPoP !== undefined) {
830
- await dpopProofJwt(headers, options.DPoP, url, 'POST');
848
+ await dpopProofJwt(headers, options.DPoP, url, 'POST', getClockSkew(client));
831
849
  }
832
850
  return authenticatedRequest(as, client, 'POST', url, parameters, headers, options);
833
851
  }
@@ -843,10 +861,14 @@ export async function refreshTokenGrantRequest(as, client, refreshToken, options
843
861
  }
844
862
  const idTokenClaims = new WeakMap();
845
863
  export function getValidatedIdTokenClaims(ref) {
846
- if (!idTokenClaims.has(ref)) {
864
+ if (!ref.id_token) {
865
+ return undefined;
866
+ }
867
+ const claims = idTokenClaims.get(ref);
868
+ if (!claims) {
847
869
  throw new TypeError('"ref" was already garbage collected or did not resolve from the proper sources');
848
870
  }
849
- return idTokenClaims.get(ref);
871
+ return claims;
850
872
  }
851
873
  async function processGenericAccessTokenResponse(as, client, response, ignoreIdToken = false, ignoreRefreshToken = false) {
852
874
  assertAs(as);
@@ -899,7 +921,7 @@ async function processGenericAccessTokenResponse(as, client, response, ignoreIdT
899
921
  throw new OPE('"response" body "id_token" property must be a non-empty string');
900
922
  }
901
923
  if (json.id_token) {
902
- const { claims } = await validateJwt(json.id_token, checkSigningAlgorithm.bind(undefined, client.id_token_signed_response_alg, as.id_token_signing_alg_values_supported), noSignatureCheck)
924
+ 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))
903
925
  .then(validatePresence.bind(undefined, ['aud', 'exp', 'iat', 'iss', 'sub']))
904
926
  .then(validateIssuer.bind(undefined, as.issuer))
905
927
  .then(validateAudience.bind(undefined, client.client_id));
@@ -1003,8 +1025,8 @@ export async function processAuthorizationCodeOpenIDResponse(as, client, respons
1003
1025
  if (typeof maxAge !== 'number' || maxAge < 0) {
1004
1026
  throw new TypeError('"options.max_age" must be a non-negative number');
1005
1027
  }
1006
- const now = epochTime();
1007
- const tolerance = 30;
1028
+ const now = epochTime() + getClockSkew(client);
1029
+ const tolerance = getClockTolerance(client);
1008
1030
  if (claims.auth_time + maxAge < now - tolerance) {
1009
1031
  throw new OPE('too much time has elapsed since the last End-User authentication');
1010
1032
  }
@@ -1131,7 +1153,7 @@ export async function processIntrospectionResponse(as, client, response) {
1131
1153
  let json;
1132
1154
  if (getContentType(response) === 'application/token-introspection+jwt') {
1133
1155
  assertReadableResponse(response);
1134
- const { claims } = await validateJwt(await response.text(), checkSigningAlgorithm.bind(undefined, client.introspection_signed_response_alg, as.introspection_signing_alg_values_supported), noSignatureCheck)
1156
+ 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))
1135
1157
  .then(checkJwtType.bind(undefined, 'token-introspection+jwt'))
1136
1158
  .then(validatePresence.bind(undefined, ['aud', 'iat', 'iss']))
1137
1159
  .then(validateIssuer.bind(undefined, as.issuer))
@@ -1279,7 +1301,7 @@ function keyToSubtle(key) {
1279
1301
  throw new UnsupportedOperationError();
1280
1302
  }
1281
1303
  const noSignatureCheck = Symbol();
1282
- async function validateJwt(jws, checkAlg, getKey) {
1304
+ async function validateJwt(jws, checkAlg, getKey, clockSkew, clockTolerance) {
1283
1305
  const { 0: protectedHeader, 1: payload, 2: encodedSignature, length } = jws.split('.');
1284
1306
  if (length === 5) {
1285
1307
  throw new UnsupportedOperationError('JWE structure JWTs are not supported');
@@ -1320,13 +1342,12 @@ async function validateJwt(jws, checkAlg, getKey) {
1320
1342
  if (!isJsonObject(claims)) {
1321
1343
  throw new OPE('JWT Payload must be a top level object');
1322
1344
  }
1323
- const now = epochTime();
1324
- const tolerance = 30;
1345
+ const now = epochTime() + clockSkew;
1325
1346
  if (claims.exp !== undefined) {
1326
1347
  if (typeof claims.exp !== 'number') {
1327
1348
  throw new OPE('unexpected JWT "exp" (expiration time) claim type');
1328
1349
  }
1329
- if (claims.exp <= now - tolerance) {
1350
+ if (claims.exp <= now - clockTolerance) {
1330
1351
  throw new OPE('unexpected JWT "exp" (expiration time) claim value, timestamp is <= now()');
1331
1352
  }
1332
1353
  }
@@ -1344,7 +1365,7 @@ async function validateJwt(jws, checkAlg, getKey) {
1344
1365
  if (typeof claims.nbf !== 'number') {
1345
1366
  throw new OPE('unexpected JWT "nbf" (not before) claim type');
1346
1367
  }
1347
- if (claims.nbf > now + tolerance) {
1368
+ if (claims.nbf > now + clockTolerance) {
1348
1369
  throw new OPE('unexpected JWT "nbf" (not before) claim value, timestamp is > now()');
1349
1370
  }
1350
1371
  }
@@ -1371,7 +1392,7 @@ export async function validateJwtAuthResponse(as, client, parameters, expectedSt
1371
1392
  if (typeof as.jwks_uri !== 'string') {
1372
1393
  throw new TypeError('"as.jwks_uri" must be a string');
1373
1394
  }
1374
- const { claims } = await validateJwt(response, checkSigningAlgorithm.bind(undefined, client.authorization_signed_response_alg, as.authorization_signing_alg_values_supported), getPublicSigKeyFromIssuerJwksUri.bind(undefined, as, options))
1395
+ 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))
1375
1396
  .then(validatePresence.bind(undefined, ['aud', 'exp', 'iss']))
1376
1397
  .then(validateIssuer.bind(undefined, as.issuer))
1377
1398
  .then(validateAudience.bind(undefined, client.client_id));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oauth4webapi",
3
- "version": "2.1.0",
3
+ "version": "2.2.1",
4
4
  "description": "OAuth 2 / OpenID Connect for Web Platform API JavaScript runtimes",
5
5
  "keywords": [
6
6
  "auth",
@@ -62,21 +62,22 @@
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.4",
70
- "esbuild": "^0.17.0",
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.11",
67
+ "@types/qunit": "^2.19.4",
68
+ "ava": "^5.2.0",
69
+ "edge-runtime": "^2.1.4",
70
+ "esbuild": "^0.17.16",
71
+ "jose": "^4.13.2",
72
+ "patch-package": "^6.5.1",
73
+ "prettier": "^2.8.7",
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.24.1",
78
+ "typedoc-plugin-markdown": "^3.15.1",
79
+ "typedoc-plugin-mdn-links": "^3.0.3",
80
+ "typescript": "^5.0.4",
81
+ "undici": "^5.21.2"
81
82
  }
82
83
  }