openid-client 4.9.0 → 5.0.2

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/lib/client.js CHANGED
@@ -1,5 +1,3 @@
1
- /* eslint-disable max-classes-per-file */
2
-
3
1
  const { inspect } = require('util');
4
2
  const stdhttp = require('http');
5
3
  const crypto = require('crypto');
@@ -7,10 +5,11 @@ const { strict: assert } = require('assert');
7
5
  const querystring = require('querystring');
8
6
  const url = require('url');
9
7
 
10
- const { ParseError } = require('got');
11
8
  const jose = require('jose');
12
9
  const tokenHash = require('oidc-token-hash');
13
10
 
11
+ const isKeyObject = require('./helpers/is_key_object');
12
+ const decodeJWT = require('./helpers/decode_jwt');
14
13
  const base64url = require('./helpers/base64url');
15
14
  const defaults = require('./helpers/defaults');
16
15
  const { assertSigningAlgValuesSupport, assertIssuerConfiguration } = require('./helpers/assert');
@@ -22,44 +21,42 @@ const { OPError, RPError } = require('./errors');
22
21
  const now = require('./helpers/unix_timestamp');
23
22
  const { random } = require('./helpers/generators');
24
23
  const request = require('./helpers/request');
25
- const {
26
- CALLBACK_PROPERTIES, CLIENT_DEFAULTS, JWT_CONTENT, CLOCK_TOLERANCE,
27
- } = require('./helpers/consts');
28
- const issuerRegistry = require('./issuer_registry');
29
- const instance = require('./helpers/weak_cache');
24
+ const { CLOCK_TOLERANCE } = require('./helpers/consts');
25
+ const { keystores } = require('./helpers/weak_cache');
26
+ const KeyStore = require('./helpers/keystore');
27
+ const clone = require('./helpers/deep_clone');
30
28
  const { authenticatedPost, resolveResponseType, resolveRedirectUri } = require('./helpers/client');
29
+ const { queryKeyStore } = require('./helpers/issuer');
31
30
  const DeviceFlowHandle = require('./device_flow_handle');
32
31
 
32
+ const [major, minor] = process.version
33
+ .substr(1)
34
+ .split('.')
35
+ .map((str) => parseInt(str, 10));
36
+
37
+ const rsaPssParams = major >= 17 || (major === 16 && minor >= 9);
38
+
33
39
  function pickCb(input) {
34
- return pick(input, ...CALLBACK_PROPERTIES);
40
+ return pick(
41
+ input,
42
+ 'access_token', // OAuth 2.0
43
+ 'code', // OAuth 2.0
44
+ 'error', // OAuth 2.0
45
+ 'error_description', // OAuth 2.0
46
+ 'error_uri', // OAuth 2.0
47
+ 'expires_in', // OAuth 2.0
48
+ 'id_token', // OIDC Core 1.0
49
+ 'state', // OAuth 2.0
50
+ 'token_type', // OAuth 2.0
51
+ 'session_state', // OIDC Session Management
52
+ 'response', // FAPI JARM
53
+ );
35
54
  }
36
55
 
37
56
  function authorizationHeaderValue(token, tokenType = 'Bearer') {
38
57
  return `${tokenType} ${token}`;
39
58
  }
40
59
 
41
- function cleanUpClaims(claims) {
42
- if (Object.keys(claims._claim_names).length === 0) {
43
- delete claims._claim_names;
44
- }
45
- if (Object.keys(claims._claim_sources).length === 0) {
46
- delete claims._claim_sources;
47
- }
48
- }
49
-
50
- function assignClaim(target, source, sourceName, throwOnMissing = true) {
51
- return ([claim, inSource]) => {
52
- if (inSource === sourceName) {
53
- if (throwOnMissing && source[claim] === undefined) {
54
- throw new RPError(`expected claim "${claim}" in "${sourceName}"`);
55
- } else if (source[claim] !== undefined) {
56
- target[claim] = source[claim];
57
- }
58
- delete target._claim_names[claim];
59
- }
60
- };
61
- }
62
-
63
60
  function verifyPresence(payload, jwt, prop) {
64
61
  if (payload[prop] === undefined) {
65
62
  throw new RPError({
@@ -93,49 +90,22 @@ function authorizationParams(params) {
93
90
  return authParams;
94
91
  }
95
92
 
96
- async function claimJWT(label, jwt) {
97
- try {
98
- const { header, payload } = jose.JWT.decode(jwt, { complete: true });
99
- const { iss } = payload;
100
-
101
- if (header.alg === 'none') {
102
- return payload;
103
- }
104
-
105
- let key;
106
- if (!iss || iss === this.issuer.issuer) {
107
- key = await this.issuer.queryKeyStore(header);
108
- } else if (issuerRegistry.has(iss)) {
109
- key = await issuerRegistry.get(iss).queryKeyStore(header);
110
- } else {
111
- const discovered = await this.issuer.constructor.discover(iss);
112
- key = await discovered.queryKeyStore(header);
113
- }
114
- return jose.JWT.verify(jwt, key);
115
- } catch (err) {
116
- if (err instanceof RPError || err instanceof OPError || err.name === 'AggregateError') {
117
- throw err;
118
- } else {
119
- throw new RPError({
120
- printf: ['failed to validate the %s JWT (%s: %s)', label, err.name, err.message],
121
- jwt,
122
- });
123
- }
124
- }
125
- }
126
-
127
93
  function getKeystore(jwks) {
128
- const keystore = jose.JWKS.asKeyStore(jwks);
129
- if (keystore.all().some((key) => key.type !== 'private')) {
130
- throw new TypeError('jwks must only contain private keys');
94
+ if (
95
+ !isPlainObject(jwks) ||
96
+ !Array.isArray(jwks.keys) ||
97
+ jwks.keys.some((k) => !isPlainObject(k) || !('kty' in k))
98
+ ) {
99
+ throw new TypeError('jwks must be a JSON Web Key Set formatted object');
131
100
  }
132
- return keystore;
101
+
102
+ return KeyStore.fromJWKS(jwks, { onlyPrivate: true });
133
103
  }
134
104
 
135
105
  // if an OP doesnt support client_secret_basic but supports client_secret_post, use it instead
136
106
  // this is in place to take care of most common pitfalls when first using discovered Issuers without
137
107
  // the support for default values defined by Discovery 1.0
138
- function checkBasicSupport(client, metadata, properties) {
108
+ function checkBasicSupport(client, properties) {
139
109
  try {
140
110
  const supported = client.issuer.token_endpoint_auth_methods_supported;
141
111
  if (!supported.includes(properties.token_endpoint_auth_method)) {
@@ -147,8 +117,9 @@ function checkBasicSupport(client, metadata, properties) {
147
117
  }
148
118
 
149
119
  function handleCommonMistakes(client, metadata, properties) {
150
- if (!metadata.token_endpoint_auth_method) { // if no explicit value was provided
151
- checkBasicSupport(client, metadata, properties);
120
+ if (!metadata.token_endpoint_auth_method) {
121
+ // if no explicit value was provided
122
+ checkBasicSupport(client, properties);
152
123
  }
153
124
 
154
125
  // :fp: c'mon people... RTFM
@@ -188,36 +159,71 @@ function getDefaultsForEndpoint(endpoint, issuer, properties) {
188
159
  }
189
160
  }
190
161
 
191
- class BaseClient {}
192
-
193
- module.exports = (issuer, aadIssValidation = false) => class Client extends BaseClient {
194
- /**
195
- * @name constructor
196
- * @api public
197
- */
198
- constructor(metadata = {}, jwks, options) {
199
- super();
162
+ class BaseClient {
163
+ #metadata;
164
+ #issuer;
165
+ #aadIssValidation;
166
+ #additionalAuthorizedParties;
167
+ constructor(issuer, aadIssValidation, metadata = {}, jwks, options) {
168
+ this.#metadata = new Map();
169
+ this.#issuer = issuer;
170
+ this.#aadIssValidation = aadIssValidation;
200
171
 
201
172
  if (typeof metadata.client_id !== 'string' || !metadata.client_id) {
202
173
  throw new TypeError('client_id is required');
203
174
  }
204
175
 
205
- const properties = { ...CLIENT_DEFAULTS, ...metadata };
176
+ const properties = {
177
+ grant_types: ['authorization_code'],
178
+ id_token_signed_response_alg: 'RS256',
179
+ authorization_signed_response_alg: 'RS256',
180
+ response_types: ['code'],
181
+ token_endpoint_auth_method: 'client_secret_basic',
182
+ ...(this.fapi()
183
+ ? {
184
+ grant_types: ['authorization_code', 'implicit'],
185
+ id_token_signed_response_alg: 'PS256',
186
+ authorization_signed_response_alg: 'PS256',
187
+ response_types: ['code id_token'],
188
+ tls_client_certificate_bound_access_tokens: true,
189
+ token_endpoint_auth_method: undefined,
190
+ }
191
+ : undefined),
192
+ ...metadata,
193
+ };
194
+
195
+ if (this.fapi()) {
196
+ switch (properties.token_endpoint_auth_method) {
197
+ case 'self_signed_tls_client_auth':
198
+ case 'tls_client_auth':
199
+ break;
200
+ case 'private_key_jwt':
201
+ if (!jwks) {
202
+ throw new TypeError('jwks is required');
203
+ }
204
+ break;
205
+ case undefined:
206
+ throw new TypeError('token_endpoint_auth_method is required');
207
+ default:
208
+ throw new TypeError('invalid or unsupported token_endpoint_auth_method');
209
+ }
210
+ }
206
211
 
207
212
  handleCommonMistakes(this, metadata, properties);
208
213
 
209
214
  assertSigningAlgValuesSupport('token', this.issuer, properties);
210
-
211
215
  ['introspection', 'revocation'].forEach((endpoint) => {
212
216
  getDefaultsForEndpoint(endpoint, this.issuer, properties);
213
217
  assertSigningAlgValuesSupport(endpoint, this.issuer, properties);
214
218
  });
215
219
 
216
220
  Object.entries(properties).forEach(([key, value]) => {
217
- instance(this).get('metadata').set(key, value);
221
+ this.#metadata.set(key, value);
218
222
  if (!this[key]) {
219
223
  Object.defineProperty(this, key, {
220
- get() { return instance(this).get('metadata').get(key); },
224
+ get() {
225
+ return this.#metadata.get(key);
226
+ },
221
227
  enumerable: true,
222
228
  });
223
229
  }
@@ -225,20 +231,16 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
225
231
 
226
232
  if (jwks !== undefined) {
227
233
  const keystore = getKeystore.call(this, jwks);
228
- instance(this).set('keystore', keystore);
234
+ keystores.set(this, keystore);
229
235
  }
230
236
 
231
- if (options !== undefined) {
232
- instance(this).set('options', options);
237
+ if (options != null && options.additionalAuthorizedParties) {
238
+ this.#additionalAuthorizedParties = clone(options.additionalAuthorizedParties);
233
239
  }
234
240
 
235
241
  this[CLOCK_TOLERANCE] = 0;
236
242
  }
237
243
 
238
- /**
239
- * @name authorizationUrl
240
- * @api public
241
- */
242
244
  authorizationUrl(params = {}) {
243
245
  if (!isPlainObject(params)) {
244
246
  throw new TypeError('params must be a plain object');
@@ -253,48 +255,35 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
253
255
  return url.format(target);
254
256
  }
255
257
 
256
- /**
257
- * @name authorizationPost
258
- * @api public
259
- */
260
258
  authorizationPost(params = {}) {
261
259
  if (!isPlainObject(params)) {
262
260
  throw new TypeError('params must be a plain object');
263
261
  }
264
262
  const inputs = authorizationParams.call(this, params);
265
263
  const formInputs = Object.keys(inputs)
266
- .map((name) => `<input type="hidden" name="${name}" value="${inputs[name]}"/>`).join('\n');
264
+ .map((name) => `<input type="hidden" name="${name}" value="${inputs[name]}"/>`)
265
+ .join('\n');
267
266
 
268
267
  return `<!DOCTYPE html>
269
268
  <head>
270
- <title>Requesting Authorization</title>
269
+ <title>Requesting Authorization</title>
271
270
  </head>
272
271
  <body onload="javascript:document.forms[0].submit()">
273
- <form method="post" action="${this.issuer.authorization_endpoint}">
274
- ${formInputs}
275
- </form>
272
+ <form method="post" action="${this.issuer.authorization_endpoint}">
273
+ ${formInputs}
274
+ </form>
276
275
  </body>
277
276
  </html>`;
278
277
  }
279
278
 
280
- /**
281
- * @name endSessionUrl
282
- * @api public
283
- */
284
279
  endSessionUrl(params = {}) {
285
280
  assertIssuerConfiguration(this.issuer, 'end_session_endpoint');
286
281
 
287
- const {
288
- 0: postLogout,
289
- length,
290
- } = this.post_logout_redirect_uris || [];
282
+ const { 0: postLogout, length } = this.post_logout_redirect_uris || [];
291
283
 
292
- const {
293
- post_logout_redirect_uri = length === 1 ? postLogout : undefined,
294
- } = params;
284
+ const { post_logout_redirect_uri = length === 1 ? postLogout : undefined } = params;
295
285
 
296
286
  let hint = params.id_token_hint;
297
-
298
287
  if (hint instanceof TokenSet) {
299
288
  if (!hint.id_token) {
300
289
  throw new TypeError('id_token not present in TokenSet');
@@ -322,26 +311,25 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
322
311
  return url.format(target);
323
312
  }
324
313
 
325
- /**
326
- * @name callbackParams
327
- * @api public
328
- */
329
- callbackParams(input) { // eslint-disable-line class-methods-use-this
330
- const isIncomingMessage = input instanceof stdhttp.IncomingMessage
331
- || (input && input.method && input.url);
314
+ callbackParams(input) {
315
+ const isIncomingMessage =
316
+ input instanceof stdhttp.IncomingMessage || (input && input.method && input.url);
332
317
  const isString = typeof input === 'string';
333
318
 
334
319
  if (!isString && !isIncomingMessage) {
335
- throw new TypeError('#callbackParams only accepts string urls, http.IncomingMessage or a lookalike');
320
+ throw new TypeError(
321
+ '#callbackParams only accepts string urls, http.IncomingMessage or a lookalike',
322
+ );
336
323
  }
337
-
338
324
  if (isIncomingMessage) {
339
325
  switch (input.method) {
340
326
  case 'GET':
341
327
  return pickCb(url.parse(input.url, true).query);
342
328
  case 'POST':
343
329
  if (input.body === undefined) {
344
- throw new TypeError('incoming message body missing, include a body parser prior to this method call');
330
+ throw new TypeError(
331
+ 'incoming message body missing, include a body parser prior to this method call',
332
+ );
345
333
  }
346
334
  switch (typeof input.body) {
347
335
  case 'object':
@@ -365,10 +353,6 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
365
353
  }
366
354
  }
367
355
 
368
- /**
369
- * @name callback
370
- * @api public
371
- */
372
356
  async callback(
373
357
  redirectUri,
374
358
  parameters,
@@ -423,7 +407,7 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
423
407
  };
424
408
 
425
409
  if (checks.response_type) {
426
- for (const type of checks.response_type.split(' ')) { // eslint-disable-line no-restricted-syntax
410
+ for (const type of checks.response_type.split(' ')) {
427
411
  if (type === 'none') {
428
412
  if (params.code || params.id_token || params.access_token) {
429
413
  throw new RPError({
@@ -433,7 +417,7 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
433
417
  });
434
418
  }
435
419
  } else {
436
- for (const param of RESPONSE_TYPE_REQUIRED_PARAMS[type]) { // eslint-disable-line no-restricted-syntax, max-len
420
+ for (const param of RESPONSE_TYPE_REQUIRED_PARAMS[type]) {
437
421
  if (!params[param]) {
438
422
  throw new RPError({
439
423
  message: `${param} missing from response`,
@@ -449,7 +433,13 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
449
433
  if (params.id_token) {
450
434
  const tokenset = new TokenSet(params);
451
435
  await this.decryptIdToken(tokenset);
452
- await this.validateIdToken(tokenset, checks.nonce, 'authorization', checks.max_age, checks.state);
436
+ await this.validateIdToken(
437
+ tokenset,
438
+ checks.nonce,
439
+ 'authorization',
440
+ checks.max_age,
441
+ checks.state,
442
+ );
453
443
 
454
444
  if (!params.code) {
455
445
  return tokenset;
@@ -457,13 +447,16 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
457
447
  }
458
448
 
459
449
  if (params.code) {
460
- const tokenset = await this.grant({
461
- ...exchangeBody,
462
- grant_type: 'authorization_code',
463
- code: params.code,
464
- redirect_uri: redirectUri,
465
- code_verifier: checks.code_verifier,
466
- }, { clientAssertionPayload, DPoP });
450
+ const tokenset = await this.grant(
451
+ {
452
+ ...exchangeBody,
453
+ grant_type: 'authorization_code',
454
+ code: params.code,
455
+ redirect_uri: redirectUri,
456
+ code_verifier: checks.code_verifier,
457
+ },
458
+ { clientAssertionPayload, DPoP },
459
+ );
467
460
 
468
461
  await this.decryptIdToken(tokenset);
469
462
  await this.validateIdToken(tokenset, checks.nonce, 'token', checks.max_age);
@@ -472,16 +465,24 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
472
465
  tokenset.session_state = params.session_state;
473
466
  }
474
467
 
468
+ if (tokenset.scope && checks.scope && this.fapi()) {
469
+ const expected = new Set(checks.scope.split(' '));
470
+ const actual = tokenset.scope.split(' ');
471
+ if (!actual.every(Set.prototype.has, expected)) {
472
+ throw new RPError({
473
+ message: 'unexpected scope returned',
474
+ checks,
475
+ scope: tokenset.scope,
476
+ });
477
+ }
478
+ }
479
+
475
480
  return tokenset;
476
481
  }
477
482
 
478
483
  return new TokenSet(params);
479
484
  }
480
485
 
481
- /**
482
- * @name oauthCallback
483
- * @api public
484
- */
485
486
  async oauthCallback(
486
487
  redirectUri,
487
488
  parameters,
@@ -531,7 +532,7 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
531
532
  };
532
533
 
533
534
  if (checks.response_type) {
534
- for (const type of checks.response_type.split(' ')) { // eslint-disable-line no-restricted-syntax
535
+ for (const type of checks.response_type.split(' ')) {
535
536
  if (type === 'none') {
536
537
  if (params.code || params.id_token || params.access_token) {
537
538
  throw new RPError({
@@ -543,7 +544,7 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
543
544
  }
544
545
 
545
546
  if (RESPONSE_TYPE_REQUIRED_PARAMS[type]) {
546
- for (const param of RESPONSE_TYPE_REQUIRED_PARAMS[type]) { // eslint-disable-line no-restricted-syntax, max-len
547
+ for (const param of RESPONSE_TYPE_REQUIRED_PARAMS[type]) {
547
548
  if (!params[param]) {
548
549
  throw new RPError({
549
550
  message: `${param} missing from response`,
@@ -557,22 +558,35 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
557
558
  }
558
559
 
559
560
  if (params.code) {
560
- return this.grant({
561
- ...exchangeBody,
562
- grant_type: 'authorization_code',
563
- code: params.code,
564
- redirect_uri: redirectUri,
565
- code_verifier: checks.code_verifier,
566
- }, { clientAssertionPayload, DPoP });
561
+ const tokenset = await this.grant(
562
+ {
563
+ ...exchangeBody,
564
+ grant_type: 'authorization_code',
565
+ code: params.code,
566
+ redirect_uri: redirectUri,
567
+ code_verifier: checks.code_verifier,
568
+ },
569
+ { clientAssertionPayload, DPoP },
570
+ );
571
+
572
+ if (tokenset.scope && checks.scope && this.fapi()) {
573
+ const expected = new Set(checks.scope.split(' '));
574
+ const actual = tokenset.scope.split(' ');
575
+ if (!actual.every(Set.prototype.has, expected)) {
576
+ throw new RPError({
577
+ message: 'unexpected scope returned',
578
+ checks,
579
+ scope: tokenset.scope,
580
+ });
581
+ }
582
+ }
583
+
584
+ return tokenset;
567
585
  }
568
586
 
569
587
  return new TokenSet(params);
570
588
  }
571
589
 
572
- /**
573
- * @name decryptIdToken
574
- * @api private
575
- */
576
590
  async decryptIdToken(token) {
577
591
  if (!this.id_token_encrypted_response_alg) {
578
592
  return token;
@@ -606,10 +620,6 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
606
620
  return this.validateJWT(body, expectedAlg, []);
607
621
  }
608
622
 
609
- /**
610
- * @name decryptJARM
611
- * @api private
612
- */
613
623
  async decryptJARM(response) {
614
624
  if (!this.authorization_encrypted_response_alg) {
615
625
  return response;
@@ -621,10 +631,6 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
621
631
  return this.decryptJWE(response, expectedAlg, expectedEnc);
622
632
  }
623
633
 
624
- /**
625
- * @name decryptJWTUserinfo
626
- * @api private
627
- */
628
634
  async decryptJWTUserinfo(body) {
629
635
  if (!this.userinfo_encrypted_response_alg) {
630
636
  return body;
@@ -636,10 +642,6 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
636
642
  return this.decryptJWE(body, expectedAlg, expectedEnc);
637
643
  }
638
644
 
639
- /**
640
- * @name decryptJWE
641
- * @api private
642
- */
643
645
  async decryptJWE(jwe, expectedAlg, expectedEnc = 'A128CBC-HS256') {
644
646
  const header = JSON.parse(base64url.decode(jwe.split('.')[0]));
645
647
 
@@ -657,22 +659,33 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
657
659
  });
658
660
  }
659
661
 
660
- let keyOrStore;
661
-
662
+ const getPlaintext = (result) => new TextDecoder().decode(result.plaintext);
663
+ let plaintext;
662
664
  if (expectedAlg.match(/^(?:RSA|ECDH)/)) {
663
- keyOrStore = instance(this).get('keystore');
665
+ const keystore = await keystores.get(this);
666
+
667
+ for (const { keyObject: key } of keystore.all({
668
+ ...jose.decodeProtectedHeader(jwe),
669
+ use: 'enc',
670
+ })) {
671
+ plaintext = await jose.compactDecrypt(jwe, key).then(getPlaintext, () => {});
672
+ if (plaintext) break;
673
+ }
664
674
  } else {
665
- keyOrStore = await this.joseSecret(expectedAlg === 'dir' ? expectedEnc : expectedAlg);
675
+ plaintext = await jose
676
+ .compactDecrypt(jwe, this.secretForAlg(expectedAlg === 'dir' ? expectedEnc : expectedAlg))
677
+ .then(getPlaintext, () => {});
666
678
  }
667
679
 
668
- const payload = jose.JWE.decrypt(jwe, keyOrStore);
669
- return payload.toString('utf8');
680
+ if (!plaintext) {
681
+ throw new RPError({
682
+ message: 'failed to decrypt JWE',
683
+ jwt: jwe,
684
+ });
685
+ }
686
+ return plaintext;
670
687
  }
671
688
 
672
- /**
673
- * @name validateIdToken
674
- * @api private
675
- */
676
689
  async validateIdToken(tokenSet, nonce, returnedBy, maxAge, state) {
677
690
  let idToken = tokenSet;
678
691
 
@@ -707,9 +720,14 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
707
720
  }
708
721
  }
709
722
 
710
- if (maxAge && (payload.auth_time + maxAge < timestamp - this[CLOCK_TOLERANCE])) {
723
+ if (maxAge && payload.auth_time + maxAge < timestamp - this[CLOCK_TOLERANCE]) {
711
724
  throw new RPError({
712
- printf: ['too much time has elapsed since the last End-User authentication, max_age %i, auth_time: %i, now %i', maxAge, payload.auth_time, timestamp - this[CLOCK_TOLERANCE]],
725
+ printf: [
726
+ 'too much time has elapsed since the last End-User authentication, max_age %i, auth_time: %i, now %i',
727
+ maxAge,
728
+ payload.auth_time,
729
+ timestamp - this[CLOCK_TOLERANCE],
730
+ ],
713
731
  now: timestamp,
714
732
  tolerance: this[CLOCK_TOLERANCE],
715
733
  auth_time: payload.auth_time,
@@ -724,8 +742,6 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
724
742
  });
725
743
  }
726
744
 
727
- const fapi = this.constructor.name === 'FAPIClient';
728
-
729
745
  if (returnedBy === 'authorization') {
730
746
  if (!payload.at_hash && tokenSet.access_token) {
731
747
  throw new RPError({
@@ -741,7 +757,7 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
741
757
  });
742
758
  }
743
759
 
744
- if (fapi) {
760
+ if (this.fapi()) {
745
761
  if (!payload.s_hash && (tokenSet.state || state)) {
746
762
  throw new RPError({
747
763
  message: 'missing required property s_hash',
@@ -756,14 +772,20 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
756
772
  }
757
773
 
758
774
  try {
759
- tokenHash.validate({ claim: 's_hash', source: 'state' }, payload.s_hash, state, header.alg, key && key.crv);
775
+ tokenHash.validate(
776
+ { claim: 's_hash', source: 'state' },
777
+ payload.s_hash,
778
+ state,
779
+ header.alg,
780
+ key.jwk && key.jwk.crv,
781
+ );
760
782
  } catch (err) {
761
783
  throw new RPError({ message: err.message, jwt: idToken });
762
784
  }
763
785
  }
764
786
  }
765
787
 
766
- if (fapi && payload.iat < timestamp - 3600) {
788
+ if (this.fapi() && payload.iat < timestamp - 3600) {
767
789
  throw new RPError({
768
790
  printf: ['JWT issued too far in the past, now %i, iat %i', timestamp, payload.iat],
769
791
  now: timestamp,
@@ -775,7 +797,13 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
775
797
 
776
798
  if (tokenSet.access_token && payload.at_hash !== undefined) {
777
799
  try {
778
- tokenHash.validate({ claim: 'at_hash', source: 'access_token' }, payload.at_hash, tokenSet.access_token, header.alg, key && key.crv);
800
+ tokenHash.validate(
801
+ { claim: 'at_hash', source: 'access_token' },
802
+ payload.at_hash,
803
+ tokenSet.access_token,
804
+ header.alg,
805
+ key.jwk && key.jwk.crv,
806
+ );
779
807
  } catch (err) {
780
808
  throw new RPError({ message: err.message, jwt: idToken });
781
809
  }
@@ -783,7 +811,13 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
783
811
 
784
812
  if (tokenSet.code && payload.c_hash !== undefined) {
785
813
  try {
786
- tokenHash.validate({ claim: 'c_hash', source: 'code' }, payload.c_hash, tokenSet.code, header.alg, key && key.crv);
814
+ tokenHash.validate(
815
+ { claim: 'c_hash', source: 'code' },
816
+ payload.c_hash,
817
+ tokenSet.code,
818
+ header.alg,
819
+ key.jwk && key.jwk.crv,
820
+ );
787
821
  } catch (err) {
788
822
  throw new RPError({ message: err.message, jwt: idToken });
789
823
  }
@@ -792,17 +826,13 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
792
826
  return tokenSet;
793
827
  }
794
828
 
795
- /**
796
- * @name validateJWT
797
- * @api private
798
- */
799
829
  async validateJWT(jwt, expectedAlg, required = ['iss', 'sub', 'aud', 'exp', 'iat']) {
800
830
  const isSelfIssued = this.issuer.issuer === 'https://self-issued.me';
801
831
  const timestamp = now();
802
832
  let header;
803
833
  let payload;
804
834
  try {
805
- ({ header, payload } = jose.JWT.decode(jwt, { complete: true }));
835
+ ({ header, payload } = decodeJWT(jwt, { complete: true }));
806
836
  } catch (err) {
807
837
  throw new RPError({
808
838
  printf: ['failed to decode JWT (%s: %s)', err.name, err.message],
@@ -818,7 +848,7 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
818
848
  }
819
849
 
820
850
  if (isSelfIssued) {
821
- required = [...required, 'sub_jwk']; // eslint-disable-line no-param-reassign
851
+ required = [...required, 'sub_jwk'];
822
852
  }
823
853
 
824
854
  required.forEach(verifyPresence.bind(undefined, payload, jwt));
@@ -826,7 +856,7 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
826
856
  if (payload.iss !== undefined) {
827
857
  let expectedIss = this.issuer.issuer;
828
858
 
829
- if (aadIssValidation) {
859
+ if (this.#aadIssValidation) {
830
860
  expectedIss = this.issuer.issuer.replace('{tenantid}', payload.tid);
831
861
  }
832
862
 
@@ -856,7 +886,11 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
856
886
  }
857
887
  if (payload.nbf > timestamp + this[CLOCK_TOLERANCE]) {
858
888
  throw new RPError({
859
- printf: ['JWT not active yet, now %i, nbf %i', timestamp + this[CLOCK_TOLERANCE], payload.nbf],
889
+ printf: [
890
+ 'JWT not active yet, now %i, nbf %i',
891
+ timestamp + this[CLOCK_TOLERANCE],
892
+ payload.nbf,
893
+ ],
860
894
  now: timestamp,
861
895
  tolerance: this[CLOCK_TOLERANCE],
862
896
  nbf: payload.nbf,
@@ -894,7 +928,11 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
894
928
 
895
929
  if (!payload.aud.includes(this.client_id)) {
896
930
  throw new RPError({
897
- printf: ['aud is missing the client_id, expected %s to be included in %j', this.client_id, payload.aud],
931
+ printf: [
932
+ 'aud is missing the client_id, expected %s to be included in %j',
933
+ this.client_id,
934
+ payload.aud,
935
+ ],
898
936
  jwt,
899
937
  });
900
938
  }
@@ -907,7 +945,7 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
907
945
  }
908
946
 
909
947
  if (payload.azp !== undefined) {
910
- let { additionalAuthorizedParties } = instance(this).get('options') || {};
948
+ let additionalAuthorizedParties = this.#additionalAuthorizedParties;
911
949
 
912
950
  if (typeof additionalAuthorizedParties === 'string') {
913
951
  additionalAuthorizedParties = [this.client_id, additionalAuthorizedParties];
@@ -925,52 +963,55 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
925
963
  }
926
964
  }
927
965
 
928
- let key;
966
+ let keys;
929
967
 
930
968
  if (isSelfIssued) {
931
969
  try {
932
970
  assert(isPlainObject(payload.sub_jwk));
933
- key = jose.JWK.asKey(payload.sub_jwk);
971
+ const key = await jose.importJWK(payload.sub_jwk, header.alg);
934
972
  assert.equal(key.type, 'public');
973
+ keys = [{ keyObject: key }];
935
974
  } catch (err) {
936
975
  throw new RPError({
937
976
  message: 'failed to use sub_jwk claim as an asymmetric JSON Web Key',
938
977
  jwt,
939
978
  });
940
979
  }
941
- if (key.thumbprint !== payload.sub) {
980
+ if ((await jose.calculateJwkThumbprint(payload.sub_jwk)) !== payload.sub) {
942
981
  throw new RPError({
943
982
  message: 'failed to match the subject with sub_jwk',
944
983
  jwt,
945
984
  });
946
985
  }
947
986
  } else if (header.alg.startsWith('HS')) {
948
- key = await this.joseSecret();
987
+ keys = [this.secretForAlg(header.alg)];
949
988
  } else if (header.alg !== 'none') {
950
- key = await this.issuer.queryKeyStore(header);
989
+ keys = await queryKeyStore.call(this.issuer, { ...header, use: 'sig' });
951
990
  }
952
991
 
953
- if (!key && header.alg === 'none') {
992
+ if (!keys && header.alg === 'none') {
954
993
  return { protected: header, payload };
955
994
  }
956
995
 
957
- try {
958
- return {
959
- ...jose.JWS.verify(jwt, key, { complete: true }),
960
- payload,
961
- };
962
- } catch (err) {
963
- throw new RPError({
964
- message: 'failed to validate JWT signature',
965
- jwt,
966
- });
996
+ for (const key of keys) {
997
+ const verified = await jose
998
+ .compactVerify(jwt, key instanceof Uint8Array ? key : key.keyObject)
999
+ .catch(() => {});
1000
+ if (verified) {
1001
+ return {
1002
+ payload,
1003
+ protected: verified.protectedHeader,
1004
+ key,
1005
+ };
1006
+ }
967
1007
  }
1008
+
1009
+ throw new RPError({
1010
+ message: 'failed to validate JWT signature',
1011
+ jwt,
1012
+ });
968
1013
  }
969
1014
 
970
- /**
971
- * @name refresh
972
- * @api public
973
- */
974
1015
  async refresh(refreshToken, { exchangeBody, clientAssertionPayload, DPoP } = {}) {
975
1016
  let token = refreshToken;
976
1017
 
@@ -981,11 +1022,14 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
981
1022
  token = token.refresh_token;
982
1023
  }
983
1024
 
984
- const tokenset = await this.grant({
985
- ...exchangeBody,
986
- grant_type: 'refresh_token',
987
- refresh_token: String(token),
988
- }, { clientAssertionPayload, DPoP });
1025
+ const tokenset = await this.grant(
1026
+ {
1027
+ ...exchangeBody,
1028
+ grant_type: 'refresh_token',
1029
+ refresh_token: String(token),
1030
+ },
1031
+ { clientAssertionPayload, DPoP },
1032
+ );
989
1033
 
990
1034
  if (tokenset.id_token) {
991
1035
  await this.decryptIdToken(tokenset);
@@ -1014,15 +1058,24 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
1014
1058
  headers,
1015
1059
  body,
1016
1060
  DPoP,
1017
- // eslint-disable-next-line no-nested-ternary
1018
- tokenType = DPoP ? 'DPoP' : accessToken instanceof TokenSet ? accessToken.token_type : 'Bearer',
1061
+ tokenType = DPoP
1062
+ ? 'DPoP'
1063
+ : accessToken instanceof TokenSet
1064
+ ? accessToken.token_type
1065
+ : 'Bearer',
1019
1066
  } = {},
1020
1067
  ) {
1021
1068
  if (accessToken instanceof TokenSet) {
1022
1069
  if (!accessToken.access_token) {
1023
1070
  throw new TypeError('access_token not present in TokenSet');
1024
1071
  }
1025
- accessToken = accessToken.access_token; // eslint-disable-line no-param-reassign
1072
+ accessToken = accessToken.access_token;
1073
+ }
1074
+
1075
+ if (!accessToken) {
1076
+ throw new TypeError('no access token provided');
1077
+ } else if (typeof accessToken !== 'string') {
1078
+ throw new TypeError('invalid access token provided');
1026
1079
  }
1027
1080
 
1028
1081
  const requestOpts = {
@@ -1035,21 +1088,19 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
1035
1088
 
1036
1089
  const mTLS = !!this.tls_client_certificate_bound_access_tokens;
1037
1090
 
1038
- return request.call(this, {
1039
- ...requestOpts,
1040
- responseType: 'buffer',
1041
- method,
1042
- url: resourceUrl,
1043
- }, { accessToken, mTLS, DPoP });
1091
+ return request.call(
1092
+ this,
1093
+ {
1094
+ ...requestOpts,
1095
+ responseType: 'buffer',
1096
+ method,
1097
+ url: resourceUrl,
1098
+ },
1099
+ { accessToken, mTLS, DPoP },
1100
+ );
1044
1101
  }
1045
1102
 
1046
- /**
1047
- * @name userinfo
1048
- * @api public
1049
- */
1050
- async userinfo(accessToken, {
1051
- method = 'GET', via = 'header', tokenType, params, DPoP,
1052
- } = {}) {
1103
+ async userinfo(accessToken, { method = 'GET', via = 'header', tokenType, params, DPoP } = {}) {
1053
1104
  assertIssuerConfiguration(this.issuer, 'userinfo_endpoint');
1054
1105
  const options = {
1055
1106
  tokenType,
@@ -1061,9 +1112,7 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
1061
1112
  throw new TypeError('#userinfo() method can only be POST or a GET');
1062
1113
  }
1063
1114
 
1064
- if (via === 'query' && options.method !== 'GET') {
1065
- throw new TypeError('userinfo endpoints will only parse query strings for GET requests');
1066
- } else if (via === 'body' && options.method !== 'POST') {
1115
+ if (via === 'body' && options.method !== 'POST') {
1067
1116
  throw new TypeError('can only send body on POST');
1068
1117
  }
1069
1118
 
@@ -1074,7 +1123,6 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
1074
1123
  } else {
1075
1124
  options.headers = { Accept: 'application/json' };
1076
1125
  }
1077
-
1078
1126
  const mTLS = !!this.tls_client_certificate_bound_access_tokens;
1079
1127
 
1080
1128
  let targetUrl;
@@ -1084,16 +1132,14 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
1084
1132
 
1085
1133
  targetUrl = new url.URL(targetUrl || this.issuer.userinfo_endpoint);
1086
1134
 
1087
- // when via is not header we clear the Authorization header and add either
1088
- // query string parameters or urlencoded body access_token parameter
1089
- if (via === 'query') {
1090
- options.headers.Authorization = undefined;
1091
- targetUrl.searchParams.append('access_token', accessToken instanceof TokenSet ? accessToken.access_token : accessToken);
1092
- } else if (via === 'body') {
1135
+ if (via === 'body') {
1093
1136
  options.headers.Authorization = undefined;
1094
1137
  options.headers['Content-Type'] = 'application/x-www-form-urlencoded';
1095
1138
  options.body = new url.URLSearchParams();
1096
- options.body.append('access_token', accessToken instanceof TokenSet ? accessToken.access_token : accessToken);
1139
+ options.body.append(
1140
+ 'access_token',
1141
+ accessToken instanceof TokenSet ? accessToken.access_token : accessToken,
1142
+ );
1097
1143
  }
1098
1144
 
1099
1145
  // handle additional parameters, GET via querystring, POST via urlencoded body
@@ -1102,11 +1148,13 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
1102
1148
  Object.entries(params).forEach(([key, value]) => {
1103
1149
  targetUrl.searchParams.append(key, value);
1104
1150
  });
1105
- } else if (options.body) { // POST && via body
1151
+ } else if (options.body) {
1152
+ // POST && via body
1106
1153
  Object.entries(params).forEach(([key, value]) => {
1107
1154
  options.body.append(key, value);
1108
1155
  });
1109
- } else { // POST && via header
1156
+ } else {
1157
+ // POST && via header
1110
1158
  options.body = new url.URLSearchParams();
1111
1159
  options.headers['Content-Type'] = 'application/x-www-form-urlencoded';
1112
1160
  Object.entries(params).forEach(([key, value]) => {
@@ -1124,7 +1172,7 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
1124
1172
  let parsed = processResponse(response, { bearer: true });
1125
1173
 
1126
1174
  if (jwt) {
1127
- if (!JWT_CONTENT.test(response.headers['content-type'])) {
1175
+ if (!/^application\/jwt/.test(response.headers['content-type'])) {
1128
1176
  throw new RPError({
1129
1177
  message: 'expected application/jwt response from the userinfo_endpoint',
1130
1178
  response,
@@ -1149,8 +1197,9 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
1149
1197
  } else {
1150
1198
  try {
1151
1199
  parsed = JSON.parse(response.body);
1152
- } catch (error) {
1153
- throw new ParseError(error, response);
1200
+ } catch (err) {
1201
+ Object.defineProperty(err, 'response', { value: response });
1202
+ throw err;
1154
1203
  }
1155
1204
  }
1156
1205
 
@@ -1168,62 +1217,40 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
1168
1217
  return parsed;
1169
1218
  }
1170
1219
 
1171
- /**
1172
- * @name derivedKey
1173
- * @api private
1174
- */
1175
- async derivedKey(len) {
1176
- const cacheKey = `${len}_key`;
1177
- if (instance(this).has(cacheKey)) {
1178
- return instance(this).get(cacheKey);
1179
- }
1180
-
1181
- const hash = len <= 256 ? 'sha256' : len <= 384 ? 'sha384' : len <= 512 ? 'sha512' : false; // eslint-disable-line no-nested-ternary
1220
+ encryptionSecret(len) {
1221
+ const hash = len <= 256 ? 'sha256' : len <= 384 ? 'sha384' : len <= 512 ? 'sha512' : false;
1182
1222
  if (!hash) {
1183
1223
  throw new Error('unsupported symmetric encryption key derivation');
1184
1224
  }
1185
1225
 
1186
- const derivedBuffer = crypto.createHash(hash)
1226
+ return crypto
1227
+ .createHash(hash)
1187
1228
  .update(this.client_secret)
1188
1229
  .digest()
1189
1230
  .slice(0, len / 8);
1190
-
1191
- const key = jose.JWK.asKey({ k: base64url.encode(derivedBuffer), kty: 'oct' });
1192
- instance(this).set(cacheKey, key);
1193
-
1194
- return key;
1195
1231
  }
1196
1232
 
1197
- /**
1198
- * @name joseSecret
1199
- * @api private
1200
- */
1201
- async joseSecret(alg) {
1233
+ secretForAlg(alg) {
1202
1234
  if (!this.client_secret) {
1203
1235
  throw new TypeError('client_secret is required');
1204
1236
  }
1237
+
1205
1238
  if (/^A(\d{3})(?:GCM)?KW$/.test(alg)) {
1206
- return this.derivedKey(parseInt(RegExp.$1, 10));
1239
+ return this.encryptionSecret(parseInt(RegExp.$1, 10));
1207
1240
  }
1208
1241
 
1209
1242
  if (/^A(\d{3})(?:GCM|CBC-HS(\d{3}))$/.test(alg)) {
1210
- return this.derivedKey(parseInt(RegExp.$2 || RegExp.$1, 10));
1211
- }
1212
-
1213
- if (instance(this).has('jose_secret')) {
1214
- return instance(this).get('jose_secret');
1243
+ return this.encryptionSecret(parseInt(RegExp.$2 || RegExp.$1, 10));
1215
1244
  }
1216
1245
 
1217
- const key = jose.JWK.asKey({ k: base64url.encode(this.client_secret), kty: 'oct' });
1218
- instance(this).set('jose_secret', key);
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);
1219
1250
 
1220
- return key;
1251
+ return secret;
1221
1252
  }
1222
1253
 
1223
- /**
1224
- * @name grant
1225
- * @api public
1226
- */
1227
1254
  async grant(body, { clientAssertionPayload, DPoP } = {}) {
1228
1255
  assertIssuerConfiguration(this.issuer, 'token_endpoint');
1229
1256
  const response = await authenticatedPost.call(
@@ -1240,10 +1267,6 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
1240
1267
  return new TokenSet(responseBody);
1241
1268
  }
1242
1269
 
1243
- /**
1244
- * @name deviceAuthorization
1245
- * @api public
1246
- */
1247
1270
  async deviceAuthorization(params = {}, { exchangeBody, clientAssertionPayload, DPoP } = {}) {
1248
1271
  assertIssuerConfiguration(this.issuer, 'device_authorization_endpoint');
1249
1272
  assertIssuerConfiguration(this.issuer, 'token_endpoint');
@@ -1276,10 +1299,6 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
1276
1299
  });
1277
1300
  }
1278
1301
 
1279
- /**
1280
- * @name revoke
1281
- * @api public
1282
- */
1283
1302
  async revoke(token, hint, { revokeBody, clientAssertionPayload } = {}) {
1284
1303
  assertIssuerConfiguration(this.issuer, 'revocation_endpoint');
1285
1304
  if (hint !== undefined && typeof hint !== 'string') {
@@ -1294,17 +1313,15 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
1294
1313
 
1295
1314
  const response = await authenticatedPost.call(
1296
1315
  this,
1297
- 'revocation', {
1316
+ 'revocation',
1317
+ {
1298
1318
  form,
1299
- }, { clientAssertionPayload },
1319
+ },
1320
+ { clientAssertionPayload },
1300
1321
  );
1301
1322
  processResponse(response, { body: false });
1302
1323
  }
1303
1324
 
1304
- /**
1305
- * @name introspect
1306
- * @api public
1307
- */
1308
1325
  async introspect(token, hint, { introspectBody, clientAssertionPayload } = {}) {
1309
1326
  assertIssuerConfiguration(this.issuer, 'introspection_endpoint');
1310
1327
  if (hint !== undefined && typeof hint !== 'string') {
@@ -1328,110 +1345,25 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
1328
1345
  return responseBody;
1329
1346
  }
1330
1347
 
1331
- /**
1332
- * @name fetchDistributedClaims
1333
- * @api public
1334
- */
1335
- async fetchDistributedClaims(claims, tokens = {}) {
1336
- if (!isPlainObject(claims)) {
1337
- throw new TypeError('claims argument must be a plain object');
1338
- }
1339
-
1340
- if (!isPlainObject(claims._claim_sources)) {
1341
- return claims;
1342
- }
1343
-
1344
- if (!isPlainObject(claims._claim_names)) {
1345
- return claims;
1346
- }
1347
-
1348
- const distributedSources = Object.entries(claims._claim_sources)
1349
- .filter(([, value]) => value && value.endpoint);
1350
-
1351
- await Promise.all(distributedSources.map(async ([sourceName, def]) => {
1352
- try {
1353
- const requestOpts = {
1354
- headers: {
1355
- Accept: 'application/jwt',
1356
- Authorization: authorizationHeaderValue(def.access_token || tokens[sourceName]),
1357
- },
1358
- };
1359
-
1360
- const response = await request.call(this, {
1361
- ...requestOpts,
1362
- method: 'GET',
1363
- url: def.endpoint,
1364
- });
1365
- const body = processResponse(response, { bearer: true });
1366
-
1367
- const decoded = await claimJWT.call(this, 'distributed', body);
1368
- delete claims._claim_sources[sourceName];
1369
- Object.entries(claims._claim_names).forEach(
1370
- assignClaim(claims, decoded, sourceName, false),
1371
- );
1372
- } catch (err) {
1373
- err.src = sourceName;
1374
- throw err;
1375
- }
1376
- }));
1377
-
1378
- cleanUpClaims(claims);
1379
- return claims;
1380
- }
1381
-
1382
- /**
1383
- * @name unpackAggregatedClaims
1384
- * @api public
1385
- */
1386
- async unpackAggregatedClaims(claims) {
1387
- if (!isPlainObject(claims)) {
1388
- throw new TypeError('claims argument must be a plain object');
1389
- }
1390
-
1391
- if (!isPlainObject(claims._claim_sources)) {
1392
- return claims;
1393
- }
1394
-
1395
- if (!isPlainObject(claims._claim_names)) {
1396
- return claims;
1397
- }
1398
-
1399
- const aggregatedSources = Object.entries(claims._claim_sources)
1400
- .filter(([, value]) => value && value.JWT);
1401
-
1402
- await Promise.all(aggregatedSources.map(async ([sourceName, def]) => {
1403
- try {
1404
- const decoded = await claimJWT.call(this, 'aggregated', def.JWT);
1405
- delete claims._claim_sources[sourceName];
1406
- Object.entries(claims._claim_names).forEach(assignClaim(claims, decoded, sourceName));
1407
- } catch (err) {
1408
- err.src = sourceName;
1409
- throw err;
1410
- }
1411
- }));
1412
-
1413
- cleanUpClaims(claims);
1414
- return claims;
1415
- }
1416
-
1417
- /**
1418
- * @name register
1419
- * @api public
1420
- */
1421
1348
  static async register(metadata, options = {}) {
1422
1349
  const { initialAccessToken, jwks, ...clientOptions } = options;
1423
1350
 
1424
1351
  assertIssuerConfiguration(this.issuer, 'registration_endpoint');
1425
1352
 
1426
1353
  if (jwks !== undefined && !(metadata.jwks || metadata.jwks_uri)) {
1427
- const keystore = getKeystore.call(this, jwks);
1428
- metadata.jwks = keystore.toJWKS(false);
1354
+ const keystore = await getKeystore.call(this, jwks);
1355
+ metadata.jwks = keystore.toJWKS();
1429
1356
  }
1430
1357
 
1431
1358
  const response = await request.call(this, {
1432
- headers: initialAccessToken ? {
1433
- Authorization: authorizationHeaderValue(initialAccessToken),
1434
- } : undefined,
1359
+ headers: {
1360
+ Accept: 'application/json',
1361
+ ...(initialAccessToken
1362
+ ? {
1363
+ Authorization: authorizationHeaderValue(initialAccessToken),
1364
+ }
1365
+ : undefined),
1366
+ },
1435
1367
  responseType: 'json',
1436
1368
  json: metadata,
1437
1369
  url: this.issuer.registration_endpoint,
@@ -1442,80 +1374,67 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
1442
1374
  return new this(responseBody, jwks, clientOptions);
1443
1375
  }
1444
1376
 
1445
- /**
1446
- * @name metadata
1447
- * @api public
1448
- */
1449
1377
  get metadata() {
1450
- const copy = {};
1451
- instance(this).get('metadata').forEach((value, key) => {
1452
- copy[key] = value;
1453
- });
1454
- return copy;
1378
+ return clone(Object.fromEntries(this.#metadata.entries()));
1455
1379
  }
1456
1380
 
1457
- /**
1458
- * @name fromUri
1459
- * @api public
1460
- */
1461
1381
  static async fromUri(registrationClientUri, registrationAccessToken, jwks, clientOptions) {
1462
1382
  const response = await request.call(this, {
1463
1383
  method: 'GET',
1464
1384
  url: registrationClientUri,
1465
1385
  responseType: 'json',
1466
- headers: { Authorization: authorizationHeaderValue(registrationAccessToken) },
1386
+ headers: {
1387
+ Authorization: authorizationHeaderValue(registrationAccessToken),
1388
+ Accept: 'application/json',
1389
+ },
1467
1390
  });
1468
1391
  const responseBody = processResponse(response, { bearer: true });
1469
1392
 
1470
1393
  return new this(responseBody, jwks, clientOptions);
1471
1394
  }
1472
1395
 
1473
- /**
1474
- * @name requestObject
1475
- * @api public
1476
- */
1477
- async requestObject(requestObject = {}, {
1478
- sign: signingAlgorithm = this.request_object_signing_alg || 'none',
1479
- encrypt: {
1480
- alg: eKeyManagement = this.request_object_encryption_alg,
1481
- enc: eContentEncryption = this.request_object_encryption_enc || 'A128CBC-HS256',
1396
+ async requestObject(
1397
+ requestObject = {},
1398
+ {
1399
+ sign: signingAlgorithm = this.request_object_signing_alg || 'none',
1400
+ encrypt: {
1401
+ alg: eKeyManagement = this.request_object_encryption_alg,
1402
+ enc: eContentEncryption = this.request_object_encryption_enc || 'A128CBC-HS256',
1403
+ } = {},
1482
1404
  } = {},
1483
- } = {}) {
1405
+ ) {
1484
1406
  if (!isPlainObject(requestObject)) {
1485
1407
  throw new TypeError('requestObject must be a plain object');
1486
1408
  }
1487
1409
 
1488
1410
  let signed;
1489
1411
  let key;
1490
-
1491
- const fapi = this.constructor.name === 'FAPIClient';
1492
1412
  const unix = now();
1493
1413
  const header = { alg: signingAlgorithm, typ: 'oauth-authz-req+jwt' };
1494
- const payload = JSON.stringify(defaults({}, requestObject, {
1495
- iss: this.client_id,
1496
- aud: this.issuer.issuer,
1497
- client_id: this.client_id,
1498
- jti: random(),
1499
- iat: unix,
1500
- exp: unix + 300,
1501
- ...(fapi ? { nbf: unix } : undefined),
1502
- }));
1503
-
1414
+ const payload = JSON.stringify(
1415
+ defaults({}, requestObject, {
1416
+ iss: this.client_id,
1417
+ aud: this.issuer.issuer,
1418
+ client_id: this.client_id,
1419
+ jti: random(),
1420
+ iat: unix,
1421
+ exp: unix + 300,
1422
+ ...(this.fapi() ? { nbf: unix } : undefined),
1423
+ }),
1424
+ );
1504
1425
  if (signingAlgorithm === 'none') {
1505
- signed = [
1506
- base64url.encode(JSON.stringify(header)),
1507
- base64url.encode(payload),
1508
- '',
1509
- ].join('.');
1426
+ signed = [base64url.encode(JSON.stringify(header)), base64url.encode(payload), ''].join('.');
1510
1427
  } else {
1511
1428
  const symmetric = signingAlgorithm.startsWith('HS');
1512
1429
  if (symmetric) {
1513
- key = await this.joseSecret();
1430
+ key = this.secretForAlg(signingAlgorithm);
1514
1431
  } else {
1515
- const keystore = instance(this).get('keystore');
1432
+ const keystore = await keystores.get(this);
1516
1433
 
1517
1434
  if (!keystore) {
1518
- throw new TypeError(`no keystore present for client, cannot sign using alg ${signingAlgorithm}`);
1435
+ throw new TypeError(
1436
+ `no keystore present for client, cannot sign using alg ${signingAlgorithm}`,
1437
+ );
1519
1438
  }
1520
1439
  key = keystore.get({ alg: signingAlgorithm, use: 'sig' });
1521
1440
  if (!key) {
@@ -1523,10 +1442,12 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
1523
1442
  }
1524
1443
  }
1525
1444
 
1526
- signed = jose.JWS.sign(payload, key, {
1527
- ...header,
1528
- kid: symmetric ? undefined : key.kid,
1529
- });
1445
+ signed = await new jose.CompactSign(new TextEncoder().encode(payload))
1446
+ .setProtectedHeader({
1447
+ ...header,
1448
+ kid: symmetric ? undefined : key.jwk.kid,
1449
+ })
1450
+ .sign(symmetric ? key : key.keyObject);
1530
1451
  }
1531
1452
 
1532
1453
  if (!eKeyManagement) {
@@ -1536,25 +1457,23 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
1536
1457
  const fields = { alg: eKeyManagement, enc: eContentEncryption, cty: 'oauth-authz-req+jwt' };
1537
1458
 
1538
1459
  if (fields.alg.match(/^(RSA|ECDH)/)) {
1539
- [key] = await this.issuer.queryKeyStore({
1540
- alg: fields.alg,
1541
- enc: fields.enc,
1542
- use: 'enc',
1543
- }, { allowMulti: true });
1460
+ [key] = await queryKeyStore.call(
1461
+ this.issuer,
1462
+ { alg: fields.alg, use: 'enc' },
1463
+ { allowMulti: true },
1464
+ );
1544
1465
  } else {
1545
- key = await this.joseSecret(fields.alg === 'dir' ? fields.enc : fields.alg);
1466
+ key = this.secretForAlg(fields.alg === 'dir' ? fields.enc : fields.alg);
1546
1467
  }
1547
1468
 
1548
- return jose.JWE.encrypt(signed, key, {
1549
- ...fields,
1550
- kid: key.kty === 'oct' ? undefined : key.kid,
1551
- });
1469
+ return new jose.CompactEncrypt(new TextEncoder().encode(signed))
1470
+ .setProtectedHeader({
1471
+ ...fields,
1472
+ kid: key instanceof Uint8Array ? undefined : key.jwk.kid,
1473
+ })
1474
+ .encrypt(key instanceof Uint8Array ? key : key.keyObject);
1552
1475
  }
1553
1476
 
1554
- /**
1555
- * @name pushedAuthorizationRequest
1556
- * @api public
1557
- */
1558
1477
  async pushedAuthorizationRequest(params = {}, { clientAssertionPayload } = {}) {
1559
1478
  assertIssuerConfiguration(this.issuer, 'pushed_authorization_request_endpoint');
1560
1479
 
@@ -1602,20 +1521,8 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
1602
1521
  return responseBody;
1603
1522
  }
1604
1523
 
1605
- /**
1606
- * @name issuer
1607
- * @api public
1608
- */
1609
- static get issuer() {
1610
- return issuer;
1611
- }
1612
-
1613
- /**
1614
- * @name issuer
1615
- * @api public
1616
- */
1617
- get issuer() { // eslint-disable-line class-methods-use-this
1618
- return issuer;
1524
+ get issuer() {
1525
+ return this.#issuer;
1619
1526
  }
1620
1527
 
1621
1528
  /* istanbul ignore next */
@@ -1627,7 +1534,11 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base
1627
1534
  sorted: true,
1628
1535
  })}`;
1629
1536
  }
1630
- };
1537
+
1538
+ fapi() {
1539
+ return this.constructor.name === 'FAPI1Client';
1540
+ }
1541
+ }
1631
1542
 
1632
1543
  /**
1633
1544
  * @name validateJARM
@@ -1656,49 +1567,156 @@ Object.defineProperty(BaseClient.prototype, 'validateJARM', {
1656
1567
  },
1657
1568
  });
1658
1569
 
1570
+ const RSPS = /^(?:RS|PS)(?:256|384|512)$/;
1571
+ function determineRsaAlgorithm(privateKey, privateKeyInput, valuesSupported) {
1572
+ if (
1573
+ typeof privateKeyInput === 'object' &&
1574
+ typeof privateKeyInput.key === 'object' &&
1575
+ privateKeyInput.key.alg
1576
+ ) {
1577
+ return privateKeyInput.key.alg;
1578
+ }
1579
+
1580
+ if (Array.isArray(valuesSupported)) {
1581
+ let candidates = valuesSupported.filter(RegExp.prototype.test.bind(RSPS));
1582
+ if (privateKey.asymmetricKeyType === 'rsa-pss') {
1583
+ candidates = candidates.filter((value) => value.startsWith('PS'));
1584
+ }
1585
+ return ['PS256', 'PS384', 'PS512', 'RS256', 'RS384', 'RS384'].find((preferred) =>
1586
+ candidates.includes(preferred),
1587
+ );
1588
+ }
1589
+
1590
+ return 'PS256';
1591
+ }
1592
+
1593
+ const p256 = Buffer.from([42, 134, 72, 206, 61, 3, 1, 7]);
1594
+ const p384 = Buffer.from([43, 129, 4, 0, 34]);
1595
+ const p521 = Buffer.from([43, 129, 4, 0, 35]);
1596
+ const secp256k1 = Buffer.from([43, 129, 4, 0, 10]);
1597
+
1598
+ function determineEcAlgorithm(privateKey, privateKeyInput) {
1599
+ // If input was a JWK
1600
+ switch (
1601
+ typeof privateKeyInput === 'object' &&
1602
+ typeof privateKeyInput.key === 'object' &&
1603
+ privateKeyInput.key.crv
1604
+ ) {
1605
+ case 'P-256':
1606
+ return 'ES256';
1607
+ case 'secp256k1':
1608
+ return 'ES256K';
1609
+ case 'P-384':
1610
+ return 'ES384';
1611
+ case 'P-512':
1612
+ return 'ES512';
1613
+ default:
1614
+ break;
1615
+ }
1616
+
1617
+ const buf = privateKey.export({ format: 'der', type: 'pkcs8' });
1618
+ const i = buf[1] < 128 ? 17 : 18;
1619
+ const len = buf[i];
1620
+ const curveOid = buf.slice(i + 1, i + 1 + len);
1621
+ if (curveOid.equals(p256)) {
1622
+ return 'ES256';
1623
+ }
1624
+
1625
+ if (curveOid.equals(p384)) {
1626
+ return 'ES384';
1627
+ }
1628
+ if (curveOid.equals(p521)) {
1629
+ return 'ES512';
1630
+ }
1631
+
1632
+ if (curveOid.equals(secp256k1)) {
1633
+ return 'ES256K';
1634
+ }
1635
+
1636
+ throw new TypeError('unsupported DPoP private key curve');
1637
+ }
1638
+
1639
+ const jwkCache = new WeakMap();
1640
+ async function getJwk(privateKey, privateKeyInput) {
1641
+ if (
1642
+ typeof privateKeyInput === 'object' &&
1643
+ typeof privateKeyInput.key === 'object' &&
1644
+ privateKeyInput.key.crv
1645
+ ) {
1646
+ return pick(privateKeyInput.key, 'kty', 'crv', 'x', 'y', 'e', 'n');
1647
+ }
1648
+
1649
+ if (jwkCache.has(privateKeyInput)) {
1650
+ return jwkCache.get(privateKeyInput);
1651
+ }
1652
+
1653
+ const jwk = pick(await jose.exportJWK(privateKey), 'kty', 'crv', 'x', 'y', 'e', 'n');
1654
+
1655
+ if (isKeyObject(privateKeyInput)) {
1656
+ jwkCache.set(privateKeyInput, jwk);
1657
+ }
1658
+
1659
+ return jwk;
1660
+ }
1661
+
1659
1662
  /**
1660
1663
  * @name dpopProof
1661
1664
  * @api private
1662
1665
  */
1663
- function dpopProof(payload, jwk, accessToken) {
1666
+ async function dpopProof(payload, privateKeyInput, accessToken) {
1664
1667
  if (!isPlainObject(payload)) {
1665
1668
  throw new TypeError('payload must be a plain object');
1666
1669
  }
1667
1670
 
1668
- let key;
1669
- try {
1670
- key = jose.JWK.asKey(jwk);
1671
- assert(key.type === 'private');
1672
- } catch (err) {
1673
- throw new TypeError('"DPoP" option must be an asymmetric private key to sign the DPoP Proof JWT with');
1671
+ let privateKey;
1672
+ if (isKeyObject(privateKeyInput)) {
1673
+ privateKey = privateKeyInput;
1674
+ } else {
1675
+ privateKey = crypto.createPrivateKey(privateKeyInput);
1674
1676
  }
1675
1677
 
1676
- let { alg } = key;
1677
-
1678
- if (!alg && this.issuer.dpop_signing_alg_values_supported) {
1679
- const algs = key.algorithms('sign');
1680
- alg = this.issuer.dpop_signing_alg_values_supported.find((a) => algs.has(a));
1678
+ if (privateKey.type !== 'private') {
1679
+ throw new TypeError('"DPoP" option must be a private key');
1681
1680
  }
1682
-
1683
- if (!alg) {
1684
- [alg] = key.algorithms('sign');
1681
+ let alg;
1682
+ switch (privateKey.asymmetricKeyType) {
1683
+ case 'ed25519':
1684
+ case 'ed448':
1685
+ alg = 'EdDSA';
1686
+ break;
1687
+ case 'ec':
1688
+ alg = determineEcAlgorithm(privateKey, privateKeyInput);
1689
+ break;
1690
+ case 'rsa':
1691
+ case rsaPssParams && 'rsa-pss':
1692
+ alg = determineRsaAlgorithm(
1693
+ privateKey,
1694
+ privateKeyInput,
1695
+ this.issuer.dpop_signing_alg_values_supported,
1696
+ );
1697
+ break;
1698
+ default:
1699
+ throw new TypeError('unsupported DPoP private key asymmetric key type');
1685
1700
  }
1686
1701
 
1687
- let ath;
1688
- if (accessToken) {
1689
- ath = base64url.encode(crypto.createHash('sha256').update(accessToken).digest());
1702
+ if (!alg) {
1703
+ throw new TypeError('could not determine DPoP JWS Algorithm');
1690
1704
  }
1691
1705
 
1692
- return jose.JWS.sign({
1693
- iat: now(),
1694
- jti: random(),
1695
- ath,
1706
+ return new jose.SignJWT({
1707
+ ath: accessToken
1708
+ ? base64url.encode(crypto.createHash('sha256').update(accessToken).digest())
1709
+ : undefined,
1696
1710
  ...payload,
1697
- }, jwk, {
1698
- alg,
1699
- typ: 'dpop+jwt',
1700
- jwk: pick(key, 'kty', 'crv', 'x', 'y', 'e', 'n'),
1701
- });
1711
+ })
1712
+ .setProtectedHeader({
1713
+ alg,
1714
+ typ: 'dpop+jwt',
1715
+ jwk: await getJwk(privateKey, privateKeyInput),
1716
+ })
1717
+ .setIssuedAt()
1718
+ .setJti(random())
1719
+ .sign(privateKey);
1702
1720
  }
1703
1721
 
1704
1722
  Object.defineProperty(BaseClient.prototype, 'dpopProof', {
@@ -1718,4 +1736,14 @@ Object.defineProperty(BaseClient.prototype, 'dpopProof', {
1718
1736
  },
1719
1737
  });
1720
1738
 
1739
+ module.exports = (issuer, aadIssValidation = false) =>
1740
+ class Client extends BaseClient {
1741
+ constructor(...args) {
1742
+ super(issuer, aadIssValidation, ...args);
1743
+ }
1744
+
1745
+ static get issuer() {
1746
+ return issuer;
1747
+ }
1748
+ };
1721
1749
  module.exports.BaseClient = BaseClient;