openid-client 5.0.2 → 5.1.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
@@ -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');
@@ -35,21 +36,23 @@ const [major, minor] = process.version
35
36
  .map((str) => parseInt(str, 10));
36
37
 
37
38
  const rsaPssParams = major >= 17 || (major === 16 && minor >= 9);
39
+ const retryAttempt = Symbol();
38
40
 
39
41
  function pickCb(input) {
40
42
  return pick(
41
43
  input,
42
44
  'access_token', // OAuth 2.0
43
45
  'code', // OAuth 2.0
44
- 'error', // OAuth 2.0
45
46
  'error_description', // OAuth 2.0
46
47
  'error_uri', // OAuth 2.0
48
+ 'error', // OAuth 2.0
47
49
  'expires_in', // OAuth 2.0
48
50
  'id_token', // OIDC Core 1.0
51
+ 'iss', // draft-ietf-oauth-iss-auth-resp
52
+ 'response', // FAPI JARM
53
+ 'session_state', // OIDC Session Management
49
54
  'state', // OAuth 2.0
50
55
  'token_type', // OAuth 2.0
51
- 'session_state', // OIDC Session Management
52
- 'response', // FAPI JARM
53
56
  );
54
57
  }
55
58
 
@@ -396,6 +399,25 @@ class BaseClient {
396
399
  });
397
400
  }
398
401
 
402
+ if ('iss' in params) {
403
+ assertIssuerConfiguration(this.issuer, 'issuer');
404
+ if (params.iss !== this.issuer.issuer) {
405
+ throw new RPError({
406
+ printf: ['iss mismatch, expected %s, got: %s', this.issuer.issuer, params.iss],
407
+ params,
408
+ });
409
+ }
410
+ } else if (
411
+ this.issuer.authorization_response_iss_parameter_supported &&
412
+ !('id_token' in params) &&
413
+ !('response' in parameters)
414
+ ) {
415
+ throw new RPError({
416
+ message: 'iss missing from the response',
417
+ params,
418
+ });
419
+ }
420
+
399
421
  if (params.error) {
400
422
  throw new OPError(params);
401
423
  }
@@ -522,10 +544,37 @@ class BaseClient {
522
544
  });
523
545
  }
524
546
 
547
+ if ('iss' in params) {
548
+ assertIssuerConfiguration(this.issuer, 'issuer');
549
+ if (params.iss !== this.issuer.issuer) {
550
+ throw new RPError({
551
+ printf: ['iss mismatch, expected %s, got: %s', this.issuer.issuer, params.iss],
552
+ params,
553
+ });
554
+ }
555
+ } else if (
556
+ this.issuer.authorization_response_iss_parameter_supported &&
557
+ !('id_token' in params) &&
558
+ !('response' in parameters)
559
+ ) {
560
+ throw new RPError({
561
+ message: 'iss missing from the response',
562
+ params,
563
+ });
564
+ }
565
+
525
566
  if (params.error) {
526
567
  throw new OPError(params);
527
568
  }
528
569
 
570
+ if ('id_token' in params) {
571
+ throw new RPError({
572
+ message:
573
+ 'id_token detected in the response, you must use client.callback() instead of client.oauthCallback()',
574
+ params,
575
+ });
576
+ }
577
+
529
578
  const RESPONSE_TYPE_REQUIRED_PARAMS = {
530
579
  code: ['code'],
531
580
  token: ['access_token', 'token_type'],
@@ -569,6 +618,14 @@ class BaseClient {
569
618
  { clientAssertionPayload, DPoP },
570
619
  );
571
620
 
621
+ if ('id_token' in tokenset) {
622
+ throw new RPError({
623
+ message:
624
+ 'id_token detected in the response, you must use client.callback() instead of client.oauthCallback()',
625
+ params,
626
+ });
627
+ }
628
+
572
629
  if (tokenset.scope && checks.scope && this.fapi()) {
573
630
  const expected = new Set(checks.scope.split(' '));
574
631
  const actual = tokenset.scope.split(' ');
@@ -1064,6 +1121,7 @@ class BaseClient {
1064
1121
  ? accessToken.token_type
1065
1122
  : 'Bearer',
1066
1123
  } = {},
1124
+ retry,
1067
1125
  ) {
1068
1126
  if (accessToken instanceof TokenSet) {
1069
1127
  if (!accessToken.access_token) {
@@ -1088,7 +1146,7 @@ class BaseClient {
1088
1146
 
1089
1147
  const mTLS = !!this.tls_client_certificate_bound_access_tokens;
1090
1148
 
1091
- return request.call(
1149
+ const response = await request.call(
1092
1150
  this,
1093
1151
  {
1094
1152
  ...requestOpts,
@@ -1098,6 +1156,24 @@ class BaseClient {
1098
1156
  },
1099
1157
  { accessToken, mTLS, DPoP },
1100
1158
  );
1159
+
1160
+ const wwwAuthenticate = response.headers['www-authenticate'];
1161
+ if (
1162
+ retry !== retryAttempt &&
1163
+ wwwAuthenticate &&
1164
+ wwwAuthenticate.toLowerCase().startsWith('dpop ') &&
1165
+ parseWwwAuthenticate(wwwAuthenticate).error === 'use_dpop_nonce'
1166
+ ) {
1167
+ return this.requestResource(resourceUrl, accessToken, {
1168
+ method,
1169
+ headers,
1170
+ body,
1171
+ DPoP,
1172
+ tokenType,
1173
+ });
1174
+ }
1175
+
1176
+ return response;
1101
1177
  }
1102
1178
 
1103
1179
  async userinfo(accessToken, { method = 'GET', via = 'header', tokenType, params, DPoP } = {}) {
@@ -1243,15 +1319,10 @@ class BaseClient {
1243
1319
  return this.encryptionSecret(parseInt(RegExp.$2 || RegExp.$1, 10));
1244
1320
  }
1245
1321
 
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;
1322
+ return new TextEncoder().encode(this.client_secret);
1252
1323
  }
1253
1324
 
1254
- async grant(body, { clientAssertionPayload, DPoP } = {}) {
1325
+ async grant(body, { clientAssertionPayload, DPoP } = {}, retry) {
1255
1326
  assertIssuerConfiguration(this.issuer, 'token_endpoint');
1256
1327
  const response = await authenticatedPost.call(
1257
1328
  this,
@@ -1262,7 +1333,15 @@ class BaseClient {
1262
1333
  },
1263
1334
  { clientAssertionPayload, DPoP },
1264
1335
  );
1265
- const responseBody = processResponse(response);
1336
+ let responseBody;
1337
+ try {
1338
+ responseBody = processResponse(response);
1339
+ } catch (err) {
1340
+ if (retry !== retryAttempt && err instanceof OPError && err.error === 'use_dpop_nonce') {
1341
+ return this.grant(body, { clientAssertionPayload, DPoP }, retryAttempt);
1342
+ }
1343
+ throw err;
1344
+ }
1266
1345
 
1267
1346
  return new TokenSet(responseBody);
1268
1347
  }
@@ -1724,7 +1803,7 @@ Object.defineProperty(BaseClient.prototype, 'dpopProof', {
1724
1803
  configurable: true,
1725
1804
  value(...args) {
1726
1805
  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.',
1806
+ '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
1807
  'DraftWarning',
1729
1808
  );
1730
1809
  Object.defineProperty(BaseClient.prototype, 'dpopProof', {
@@ -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.0",
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"
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