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/README.md +38 -58
- package/lib/client.js +525 -497
- package/lib/device_flow_handle.js +46 -32
- package/lib/errors.js +38 -44
- package/lib/helpers/assert.js +3 -1
- package/lib/helpers/base64url.js +2 -1
- package/lib/helpers/client.js +87 -37
- package/lib/helpers/consts.js +2 -57
- package/lib/helpers/decode_jwt.js +27 -0
- package/lib/helpers/deep_clone.js +3 -1
- package/lib/helpers/defaults.js +0 -2
- package/lib/helpers/generators.js +2 -1
- package/lib/helpers/is_key_object.js +4 -0
- package/lib/helpers/issuer.js +110 -0
- package/lib/helpers/keystore.js +312 -0
- package/lib/helpers/merge.js +0 -2
- package/lib/helpers/pick.js +1 -1
- package/lib/helpers/process_response.js +24 -8
- package/lib/helpers/request.js +152 -26
- package/lib/helpers/weak_cache.js +1 -8
- package/lib/index.js +0 -2
- package/lib/index.mjs +0 -1
- package/lib/issuer.js +84 -162
- package/lib/issuer_registry.js +2 -2
- package/lib/passport_strategy.js +28 -22
- package/lib/token_set.js +2 -22
- package/package.json +8 -12
- package/types/index.d.ts +84 -454
- package/lib/helpers/is_absolute_url.js +0 -12
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
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
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(
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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,
|
|
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) {
|
|
151
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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 = {
|
|
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
|
-
|
|
221
|
+
this.#metadata.set(key, value);
|
|
218
222
|
if (!this[key]) {
|
|
219
223
|
Object.defineProperty(this, key, {
|
|
220
|
-
get() {
|
|
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
|
-
|
|
234
|
+
keystores.set(this, keystore);
|
|
229
235
|
}
|
|
230
236
|
|
|
231
|
-
if (options
|
|
232
|
-
|
|
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]}"/>`)
|
|
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
|
-
|
|
269
|
+
<title>Requesting Authorization</title>
|
|
271
270
|
</head>
|
|
272
271
|
<body onload="javascript:document.forms[0].submit()">
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
327
|
-
|
|
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(
|
|
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(
|
|
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(' ')) {
|
|
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]) {
|
|
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(
|
|
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
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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(' ')) {
|
|
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]) {
|
|
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
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
-
|
|
661
|
-
|
|
662
|
+
const getPlaintext = (result) => new TextDecoder().decode(result.plaintext);
|
|
663
|
+
let plaintext;
|
|
662
664
|
if (expectedAlg.match(/^(?:RSA|ECDH)/)) {
|
|
663
|
-
|
|
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
|
-
|
|
675
|
+
plaintext = await jose
|
|
676
|
+
.compactDecrypt(jwe, this.secretForAlg(expectedAlg === 'dir' ? expectedEnc : expectedAlg))
|
|
677
|
+
.then(getPlaintext, () => {});
|
|
666
678
|
}
|
|
667
679
|
|
|
668
|
-
|
|
669
|
-
|
|
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 &&
|
|
723
|
+
if (maxAge && payload.auth_time + maxAge < timestamp - this[CLOCK_TOLERANCE]) {
|
|
711
724
|
throw new RPError({
|
|
712
|
-
printf: [
|
|
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(
|
|
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(
|
|
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(
|
|
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 } =
|
|
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'];
|
|
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: [
|
|
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: [
|
|
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
|
|
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
|
|
966
|
+
let keys;
|
|
929
967
|
|
|
930
968
|
if (isSelfIssued) {
|
|
931
969
|
try {
|
|
932
970
|
assert(isPlainObject(payload.sub_jwk));
|
|
933
|
-
key = jose.
|
|
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 (
|
|
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
|
-
|
|
987
|
+
keys = [this.secretForAlg(header.alg)];
|
|
949
988
|
} else if (header.alg !== 'none') {
|
|
950
|
-
|
|
989
|
+
keys = await queryKeyStore.call(this.issuer, { ...header, use: 'sig' });
|
|
951
990
|
}
|
|
952
991
|
|
|
953
|
-
if (!
|
|
992
|
+
if (!keys && header.alg === 'none') {
|
|
954
993
|
return { protected: header, payload };
|
|
955
994
|
}
|
|
956
995
|
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
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
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
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
|
-
|
|
1018
|
-
|
|
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;
|
|
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(
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
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 === '
|
|
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
|
-
|
|
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(
|
|
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) {
|
|
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 {
|
|
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 (
|
|
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 (
|
|
1153
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
1218
|
-
|
|
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
|
|
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
|
-
},
|
|
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(
|
|
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:
|
|
1433
|
-
|
|
1434
|
-
|
|
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
|
-
|
|
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: {
|
|
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
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
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(
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
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 =
|
|
1430
|
+
key = this.secretForAlg(signingAlgorithm);
|
|
1514
1431
|
} else {
|
|
1515
|
-
const keystore =
|
|
1432
|
+
const keystore = await keystores.get(this);
|
|
1516
1433
|
|
|
1517
1434
|
if (!keystore) {
|
|
1518
|
-
throw new TypeError(
|
|
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.
|
|
1527
|
-
|
|
1528
|
-
|
|
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
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1460
|
+
[key] = await queryKeyStore.call(
|
|
1461
|
+
this.issuer,
|
|
1462
|
+
{ alg: fields.alg, use: 'enc' },
|
|
1463
|
+
{ allowMulti: true },
|
|
1464
|
+
);
|
|
1544
1465
|
} else {
|
|
1545
|
-
key =
|
|
1466
|
+
key = this.secretForAlg(fields.alg === 'dir' ? fields.enc : fields.alg);
|
|
1546
1467
|
}
|
|
1547
1468
|
|
|
1548
|
-
return jose.
|
|
1549
|
-
|
|
1550
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1684
|
-
|
|
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
|
-
|
|
1688
|
-
|
|
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.
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1706
|
+
return new jose.SignJWT({
|
|
1707
|
+
ath: accessToken
|
|
1708
|
+
? base64url.encode(crypto.createHash('sha256').update(accessToken).digest())
|
|
1709
|
+
: undefined,
|
|
1696
1710
|
...payload,
|
|
1697
|
-
}
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
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;
|