openid-client 5.0.2 → 5.1.3

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
@@ -45,7 +45,8 @@ openid-client.
45
45
  - [OpenID Connect RP-Initiated Logout 1.0 - draft 01][feature-rp-logout]
46
46
  - [Financial-grade API Security Profile 1.0 - Part 2: Advanced (FAPI)][feature-fapi]
47
47
  - [JWT Secured Authorization Response Mode for OAuth 2.0 (JARM) - ID1][feature-jarm]
48
- - [OAuth 2.0 Demonstration of Proof-of-Possession at the Application Layer (DPoP) - draft 03][feature-dpop]
48
+ - [OAuth 2.0 Demonstration of Proof-of-Possession at the Application Layer (DPoP) - draft 04][feature-dpop]
49
+ - [OAuth 2.0 Authorization Server Issuer Identification - draft-04][feature-iss]
49
50
 
50
51
  Updates to draft specifications (DPoP, JARM, etc) are released as MINOR library versions,
51
52
  if you utilize these specification implementations consider using the tilde `~` operator in your
@@ -276,9 +277,10 @@ See [Customizing (docs)][documentation-customizing].
276
277
  [feature-rp-logout]: https://openid.net/specs/openid-connect-rpinitiated-1_0-01.html
277
278
  [feature-jarm]: https://openid.net/specs/openid-financial-api-jarm-ID1.html
278
279
  [feature-fapi]: https://openid.net/specs/openid-financial-api-part-2-1_0.html
279
- [feature-dpop]: https://tools.ietf.org/html/draft-ietf-oauth-dpop-03
280
+ [feature-dpop]: https://tools.ietf.org/html/draft-ietf-oauth-dpop-04
280
281
  [feature-par]: https://www.rfc-editor.org/rfc/rfc9126.html
281
282
  [feature-jar]: https://www.rfc-editor.org/rfc/rfc9101.html
283
+ [feature-iss]: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-iss-auth-resp-04
282
284
  [openid-certified-link]: https://openid.net/certification/
283
285
  [passport-url]: http://passportjs.org
284
286
  [npm-url]: https://www.npmjs.com/package/openid-client
package/lib/client.js CHANGED
@@ -12,6 +12,7 @@ const isKeyObject = require('./helpers/is_key_object');
12
12
  const decodeJWT = require('./helpers/decode_jwt');
13
13
  const base64url = require('./helpers/base64url');
14
14
  const defaults = require('./helpers/defaults');
15
+ const parseWwwAuthenticate = require('./helpers/www_authenticate_parser');
15
16
  const { assertSigningAlgValuesSupport, assertIssuerConfiguration } = require('./helpers/assert');
16
17
  const pick = require('./helpers/pick');
17
18
  const isPlainObject = require('./helpers/is_plain_object');
@@ -30,26 +31,30 @@ const { queryKeyStore } = require('./helpers/issuer');
30
31
  const DeviceFlowHandle = require('./device_flow_handle');
31
32
 
32
33
  const [major, minor] = process.version
33
- .substr(1)
34
+ .slice(1)
34
35
  .split('.')
35
36
  .map((str) => parseInt(str, 10));
36
37
 
37
38
  const rsaPssParams = major >= 17 || (major === 16 && minor >= 9);
39
+ const retryAttempt = Symbol();
40
+ const skipNonceCheck = Symbol();
41
+ const skipMaxAgeCheck = Symbol();
38
42
 
39
43
  function pickCb(input) {
40
44
  return pick(
41
45
  input,
42
46
  'access_token', // OAuth 2.0
43
47
  'code', // OAuth 2.0
44
- 'error', // OAuth 2.0
45
48
  'error_description', // OAuth 2.0
46
49
  'error_uri', // OAuth 2.0
50
+ 'error', // OAuth 2.0
47
51
  'expires_in', // OAuth 2.0
48
52
  'id_token', // OIDC Core 1.0
53
+ 'iss', // draft-ietf-oauth-iss-auth-resp
54
+ 'response', // FAPI JARM
55
+ 'session_state', // OIDC Session Management
49
56
  'state', // OAuth 2.0
50
57
  'token_type', // OAuth 2.0
51
- 'session_state', // OIDC Session Management
52
- 'response', // FAPI JARM
53
58
  );
54
59
  }
55
60
 
@@ -396,6 +401,25 @@ class BaseClient {
396
401
  });
397
402
  }
398
403
 
404
+ if ('iss' in params) {
405
+ assertIssuerConfiguration(this.issuer, 'issuer');
406
+ if (params.iss !== this.issuer.issuer) {
407
+ throw new RPError({
408
+ printf: ['iss mismatch, expected %s, got: %s', this.issuer.issuer, params.iss],
409
+ params,
410
+ });
411
+ }
412
+ } else if (
413
+ this.issuer.authorization_response_iss_parameter_supported &&
414
+ !('id_token' in params) &&
415
+ !('response' in parameters)
416
+ ) {
417
+ throw new RPError({
418
+ message: 'iss missing from the response',
419
+ params,
420
+ });
421
+ }
422
+
399
423
  if (params.error) {
400
424
  throw new OPError(params);
401
425
  }
@@ -522,10 +546,37 @@ class BaseClient {
522
546
  });
523
547
  }
524
548
 
549
+ if ('iss' in params) {
550
+ assertIssuerConfiguration(this.issuer, 'issuer');
551
+ if (params.iss !== this.issuer.issuer) {
552
+ throw new RPError({
553
+ printf: ['iss mismatch, expected %s, got: %s', this.issuer.issuer, params.iss],
554
+ params,
555
+ });
556
+ }
557
+ } else if (
558
+ this.issuer.authorization_response_iss_parameter_supported &&
559
+ !('id_token' in params) &&
560
+ !('response' in parameters)
561
+ ) {
562
+ throw new RPError({
563
+ message: 'iss missing from the response',
564
+ params,
565
+ });
566
+ }
567
+
525
568
  if (params.error) {
526
569
  throw new OPError(params);
527
570
  }
528
571
 
572
+ if ('id_token' in params) {
573
+ throw new RPError({
574
+ message:
575
+ 'id_token detected in the response, you must use client.callback() instead of client.oauthCallback()',
576
+ params,
577
+ });
578
+ }
579
+
529
580
  const RESPONSE_TYPE_REQUIRED_PARAMS = {
530
581
  code: ['code'],
531
582
  token: ['access_token', 'token_type'],
@@ -569,6 +620,14 @@ class BaseClient {
569
620
  { clientAssertionPayload, DPoP },
570
621
  );
571
622
 
623
+ if ('id_token' in tokenset) {
624
+ throw new RPError({
625
+ message:
626
+ 'id_token detected in the response, you must use client.callback() instead of client.oauthCallback()',
627
+ params,
628
+ });
629
+ }
630
+
572
631
  if (tokenset.scope && checks.scope && this.fapi()) {
573
632
  const expected = new Set(checks.scope.split(' '));
574
633
  const actual = tokenset.scope.split(' ');
@@ -705,7 +764,7 @@ class BaseClient {
705
764
  const timestamp = now();
706
765
  const { protected: header, payload, key } = await this.validateJWT(idToken, expectedAlg);
707
766
 
708
- if (maxAge || (maxAge !== null && this.require_auth_time)) {
767
+ if (typeof maxAge === 'number' || (maxAge !== skipMaxAgeCheck && this.require_auth_time)) {
709
768
  if (!payload.auth_time) {
710
769
  throw new RPError({
711
770
  message: 'missing required JWT property auth_time',
@@ -720,7 +779,7 @@ class BaseClient {
720
779
  }
721
780
  }
722
781
 
723
- if (maxAge && payload.auth_time + maxAge < timestamp - this[CLOCK_TOLERANCE]) {
782
+ if (typeof maxAge === 'number' && payload.auth_time + maxAge < timestamp - this[CLOCK_TOLERANCE]) {
724
783
  throw new RPError({
725
784
  printf: [
726
785
  'too much time has elapsed since the last End-User authentication, max_age %i, auth_time: %i, now %i',
@@ -735,7 +794,7 @@ class BaseClient {
735
794
  });
736
795
  }
737
796
 
738
- if (nonce !== null && (payload.nonce || nonce !== undefined) && payload.nonce !== nonce) {
797
+ if (nonce !== skipNonceCheck && (payload.nonce || nonce !== undefined) && payload.nonce !== nonce) {
739
798
  throw new RPError({
740
799
  printf: ['nonce mismatch, expected %s, got: %s', nonce, payload.nonce],
741
800
  jwt: idToken,
@@ -1033,7 +1092,7 @@ class BaseClient {
1033
1092
 
1034
1093
  if (tokenset.id_token) {
1035
1094
  await this.decryptIdToken(tokenset);
1036
- await this.validateIdToken(tokenset, null, 'token', null);
1095
+ await this.validateIdToken(tokenset, skipNonceCheck, 'token', skipMaxAgeCheck);
1037
1096
 
1038
1097
  if (refreshToken instanceof TokenSet && refreshToken.id_token) {
1039
1098
  const expectedSub = refreshToken.claims().sub;
@@ -1064,6 +1123,7 @@ class BaseClient {
1064
1123
  ? accessToken.token_type
1065
1124
  : 'Bearer',
1066
1125
  } = {},
1126
+ retry,
1067
1127
  ) {
1068
1128
  if (accessToken instanceof TokenSet) {
1069
1129
  if (!accessToken.access_token) {
@@ -1088,7 +1148,7 @@ class BaseClient {
1088
1148
 
1089
1149
  const mTLS = !!this.tls_client_certificate_bound_access_tokens;
1090
1150
 
1091
- return request.call(
1151
+ const response = await request.call(
1092
1152
  this,
1093
1153
  {
1094
1154
  ...requestOpts,
@@ -1098,6 +1158,24 @@ class BaseClient {
1098
1158
  },
1099
1159
  { accessToken, mTLS, DPoP },
1100
1160
  );
1161
+
1162
+ const wwwAuthenticate = response.headers['www-authenticate'];
1163
+ if (
1164
+ retry !== retryAttempt &&
1165
+ wwwAuthenticate &&
1166
+ wwwAuthenticate.toLowerCase().startsWith('dpop ') &&
1167
+ parseWwwAuthenticate(wwwAuthenticate).error === 'use_dpop_nonce'
1168
+ ) {
1169
+ return this.requestResource(resourceUrl, accessToken, {
1170
+ method,
1171
+ headers,
1172
+ body,
1173
+ DPoP,
1174
+ tokenType,
1175
+ });
1176
+ }
1177
+
1178
+ return response;
1101
1179
  }
1102
1180
 
1103
1181
  async userinfo(accessToken, { method = 'GET', via = 'header', tokenType, params, DPoP } = {}) {
@@ -1243,15 +1321,10 @@ class BaseClient {
1243
1321
  return this.encryptionSecret(parseInt(RegExp.$2 || RegExp.$1, 10));
1244
1322
  }
1245
1323
 
1246
- const secret = Buffer.alloc(
1247
- Math.max(parseInt(alg.substr(-3), 10) >> 3, this.client_secret.length),
1248
- );
1249
- secret.write(this.client_secret);
1250
-
1251
- return secret;
1324
+ return new TextEncoder().encode(this.client_secret);
1252
1325
  }
1253
1326
 
1254
- async grant(body, { clientAssertionPayload, DPoP } = {}) {
1327
+ async grant(body, { clientAssertionPayload, DPoP } = {}, retry) {
1255
1328
  assertIssuerConfiguration(this.issuer, 'token_endpoint');
1256
1329
  const response = await authenticatedPost.call(
1257
1330
  this,
@@ -1262,7 +1335,15 @@ class BaseClient {
1262
1335
  },
1263
1336
  { clientAssertionPayload, DPoP },
1264
1337
  );
1265
- const responseBody = processResponse(response);
1338
+ let responseBody;
1339
+ try {
1340
+ responseBody = processResponse(response);
1341
+ } catch (err) {
1342
+ if (retry !== retryAttempt && err instanceof OPError && err.error === 'use_dpop_nonce') {
1343
+ return this.grant(body, { clientAssertionPayload, DPoP }, retryAttempt);
1344
+ }
1345
+ throw err;
1346
+ }
1266
1347
 
1267
1348
  return new TokenSet(responseBody);
1268
1349
  }
@@ -1724,7 +1805,7 @@ Object.defineProperty(BaseClient.prototype, 'dpopProof', {
1724
1805
  configurable: true,
1725
1806
  value(...args) {
1726
1807
  process.emitWarning(
1727
- 'The DPoP APIs implements an IETF draft (https://www.ietf.org/archive/id/draft-ietf-oauth-dpop-03.html). Breaking draft implementations are included as minor versions of the openid-client library, therefore, the ~ semver operator should be used and close attention be payed to library changelog as well as the drafts themselves.',
1808
+ 'The DPoP APIs implements an IETF draft (https://www.ietf.org/archive/id/draft-ietf-oauth-dpop-04.html). Breaking draft implementations are included as minor versions of the openid-client library, therefore, the ~ semver operator should be used and close attention be payed to library changelog as well as the drafts themselves.',
1728
1809
  'DraftWarning',
1729
1810
  );
1730
1811
  Object.defineProperty(BaseClient.prototype, 'dpopProof', {
@@ -38,7 +38,7 @@ async function getKeyStore(reload = false) {
38
38
  responseType: 'json',
39
39
  url: this.jwks_uri,
40
40
  headers: {
41
- Accept: 'application/json',
41
+ Accept: 'application/json, application/jwk-set+json',
42
42
  },
43
43
  })
44
44
  .finally(() => {
@@ -39,7 +39,7 @@ const keyscore = (key, { alg, use }) => {
39
39
  };
40
40
 
41
41
  function getKtyFromAlg(alg) {
42
- switch (typeof alg === 'string' && alg.substr(0, 2)) {
42
+ switch (typeof alg === 'string' && alg.slice(0, 2)) {
43
43
  case 'RS':
44
44
  case 'PS':
45
45
  return 'RSA';
@@ -68,7 +68,7 @@ function getAlgorithms(use, alg, kty, crv) {
68
68
  }
69
69
 
70
70
  if (use === 'sig' || use === undefined) {
71
- algs = algs.concat([`ES${crv.substr(-3)}`.replace('21', '12')]);
71
+ algs = algs.concat([`ES${crv.slice(-3)}`.replace('21', '12')]);
72
72
  }
73
73
 
74
74
  return new Set(algs);
@@ -1,7 +1,7 @@
1
1
  module.exports = function pick(object, ...paths) {
2
2
  const obj = {};
3
3
  for (const path of paths) {
4
- if (object[path]) {
4
+ if (object[path] !== undefined) {
5
5
  obj[path] = object[path];
6
6
  }
7
7
  }
@@ -2,17 +2,10 @@ const { STATUS_CODES } = require('http');
2
2
  const { format } = require('util');
3
3
 
4
4
  const { OPError } = require('../errors');
5
+ const parseWwwAuthenticate = require('./www_authenticate_parser');
5
6
 
6
- const REGEXP = /(\w+)=("[^"]*")/g;
7
7
  const throwAuthenticateErrors = (response) => {
8
- const params = {};
9
- try {
10
- while (REGEXP.exec(response.headers['www-authenticate']) !== null) {
11
- if (RegExp.$1 && RegExp.$2) {
12
- params[RegExp.$1] = RegExp.$2.slice(1, -1);
13
- }
14
- }
15
- } catch (err) {}
8
+ const params = parseWwwAuthenticate(response.headers['www-authenticate']);
16
9
 
17
10
  if (params.error) {
18
11
  throw new OPError(params, response);
@@ -4,6 +4,8 @@ const http = require('http');
4
4
  const https = require('https');
5
5
  const { once } = require('events');
6
6
 
7
+ const LRU = require('lru-cache');
8
+
7
9
  const pkg = require('../../package.json');
8
10
  const { RPError } = require('../errors');
9
11
 
@@ -12,6 +14,7 @@ const { deep: defaultsDeep } = require('./defaults');
12
14
  const { HTTP_OPTIONS } = require('./consts');
13
15
 
14
16
  let DEFAULT_HTTP_OPTIONS;
17
+ const NQCHAR = /^[\x21\x23-\x5B\x5D-\x7E]+$/;
15
18
 
16
19
  const allowed = [
17
20
  'agent',
@@ -52,6 +55,8 @@ function send(req, body, contentType) {
52
55
  req.end();
53
56
  }
54
57
 
58
+ const nonces = new LRU({ max: 100 });
59
+
55
60
  module.exports = async function request(options, { accessToken, mTLS = false, DPoP } = {}) {
56
61
  let url;
57
62
  try {
@@ -64,12 +69,14 @@ module.exports = async function request(options, { accessToken, mTLS = false, DP
64
69
  const optsFn = this[HTTP_OPTIONS];
65
70
  let opts = options;
66
71
 
72
+ const nonceKey = `${url.origin}${url.pathname}`;
67
73
  if (DPoP && 'dpopProof' in this) {
68
74
  opts.headers = opts.headers || {};
69
75
  opts.headers.DPoP = await this.dpopProof(
70
76
  {
71
- htu: url,
77
+ htu: url.href,
72
78
  htm: options.method,
79
+ nonce: nonces.get(nonceKey),
73
80
  },
74
81
  DPoP,
75
82
  accessToken,
@@ -173,10 +180,17 @@ module.exports = async function request(options, { accessToken, mTLS = false, DP
173
180
  }
174
181
 
175
182
  return response;
176
- })().catch((err) => {
177
- Object.defineProperty(err, 'response', { value: response });
178
- throw err;
179
- });
183
+ })()
184
+ .catch((err) => {
185
+ if (response) Object.defineProperty(err, 'response', { value: response });
186
+ throw err;
187
+ })
188
+ .finally(() => {
189
+ const dpopNonce = response && response.headers['dpop-nonce'];
190
+ if (dpopNonce && NQCHAR.test(dpopNonce)) {
191
+ nonces.set(nonceKey, dpopNonce);
192
+ }
193
+ });
180
194
  };
181
195
 
182
196
  module.exports.setDefaults = setDefaults.bind(undefined, allowed);
@@ -0,0 +1,14 @@
1
+ const REGEXP = /(\w+)=("[^"]*")/g;
2
+
3
+ module.exports = (wwwAuthenticate) => {
4
+ const params = {};
5
+ try {
6
+ while (REGEXP.exec(wwwAuthenticate) !== null) {
7
+ if (RegExp.$1 && RegExp.$2) {
8
+ params[RegExp.$1] = RegExp.$2.slice(1, -1);
9
+ }
10
+ }
11
+ } catch (err) {}
12
+
13
+ return params;
14
+ };
package/lib/issuer.js CHANGED
@@ -16,7 +16,7 @@ const AAD_MULTITENANT_DISCOVERY = [
16
16
  'https://login.microsoftonline.com/organizations/v2.0/.well-known/openid-configuration',
17
17
  'https://login.microsoftonline.com/consumers/v2.0/.well-known/openid-configuration',
18
18
  ];
19
- const AAD_MULTITENANT = Symbol('AAD_MULTITENANT');
19
+ const AAD_MULTITENANT = Symbol();
20
20
  const ISSUER_DEFAULTS = {
21
21
  claim_types_supported: ['normal'],
22
22
  claims_parameter_supported: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openid-client",
3
- "version": "5.0.2",
3
+ "version": "5.1.3",
4
4
  "description": "OpenID Connect Relying Party (RP, Client) implementation for Node.js runtime, supports passportjs",
5
5
  "keywords": [
6
6
  "auth",
@@ -51,7 +51,7 @@
51
51
  ]
52
52
  },
53
53
  "dependencies": {
54
- "jose": "^4.1.0",
54
+ "jose": "^4.1.4",
55
55
  "lru-cache": "^6.0.0",
56
56
  "object-hash": "^2.0.1",
57
57
  "oidc-token-hash": "^5.0.1"
@@ -84,7 +84,7 @@
84
84
  },
85
85
  {
86
86
  "type": "fix",
87
- "section": "Bug Fixes"
87
+ "section": "Fixes"
88
88
  },
89
89
  {
90
90
  "type": "chore",
package/types/index.d.ts CHANGED
@@ -382,6 +382,7 @@ declare class BaseClient {
382
382
  }
383
383
 
384
384
  interface DeviceFlowPollOptions {
385
+ // @ts-ignore
385
386
  signal?: AbortSignal;
386
387
  }
387
388