openid-client 5.0.2 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/lib/client.js +92 -13
- package/lib/helpers/process_response.js +2 -9
- package/lib/helpers/request.js +19 -5
- package/lib/helpers/www_authenticate_parser.js +14 -0
- package/lib/issuer.js +1 -1
- package/package.json +2 -2
- package/types/index.d.ts +1 -0
package/README.md
CHANGED
|
@@ -45,7 +45,8 @@ openid-client.
|
|
|
45
45
|
- [OpenID Connect RP-Initiated Logout 1.0 - draft 01][feature-rp-logout]
|
|
46
46
|
- [Financial-grade API Security Profile 1.0 - Part 2: Advanced (FAPI)][feature-fapi]
|
|
47
47
|
- [JWT Secured Authorization Response Mode for OAuth 2.0 (JARM) - ID1][feature-jarm]
|
|
48
|
-
- [OAuth 2.0 Demonstration of Proof-of-Possession at the Application Layer (DPoP) - draft
|
|
48
|
+
- [OAuth 2.0 Demonstration of Proof-of-Possession at the Application Layer (DPoP) - draft 04][feature-dpop]
|
|
49
|
+
- [OAuth 2.0 Authorization Server Issuer Identification - draft-04][feature-iss]
|
|
49
50
|
|
|
50
51
|
Updates to draft specifications (DPoP, JARM, etc) are released as MINOR library versions,
|
|
51
52
|
if you utilize these specification implementations consider using the tilde `~` operator in your
|
|
@@ -276,9 +277,10 @@ See [Customizing (docs)][documentation-customizing].
|
|
|
276
277
|
[feature-rp-logout]: https://openid.net/specs/openid-connect-rpinitiated-1_0-01.html
|
|
277
278
|
[feature-jarm]: https://openid.net/specs/openid-financial-api-jarm-ID1.html
|
|
278
279
|
[feature-fapi]: https://openid.net/specs/openid-financial-api-part-2-1_0.html
|
|
279
|
-
[feature-dpop]: https://tools.ietf.org/html/draft-ietf-oauth-dpop-
|
|
280
|
+
[feature-dpop]: https://tools.ietf.org/html/draft-ietf-oauth-dpop-04
|
|
280
281
|
[feature-par]: https://www.rfc-editor.org/rfc/rfc9126.html
|
|
281
282
|
[feature-jar]: https://www.rfc-editor.org/rfc/rfc9101.html
|
|
283
|
+
[feature-iss]: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-iss-auth-resp-04
|
|
282
284
|
[openid-certified-link]: https://openid.net/certification/
|
|
283
285
|
[passport-url]: http://passportjs.org
|
|
284
286
|
[npm-url]: https://www.npmjs.com/package/openid-client
|
package/lib/client.js
CHANGED
|
@@ -12,6 +12,7 @@ const isKeyObject = require('./helpers/is_key_object');
|
|
|
12
12
|
const decodeJWT = require('./helpers/decode_jwt');
|
|
13
13
|
const base64url = require('./helpers/base64url');
|
|
14
14
|
const defaults = require('./helpers/defaults');
|
|
15
|
+
const parseWwwAuthenticate = require('./helpers/www_authenticate_parser');
|
|
15
16
|
const { assertSigningAlgValuesSupport, assertIssuerConfiguration } = require('./helpers/assert');
|
|
16
17
|
const pick = require('./helpers/pick');
|
|
17
18
|
const isPlainObject = require('./helpers/is_plain_object');
|
|
@@ -35,21 +36,23 @@ const [major, minor] = process.version
|
|
|
35
36
|
.map((str) => parseInt(str, 10));
|
|
36
37
|
|
|
37
38
|
const rsaPssParams = major >= 17 || (major === 16 && minor >= 9);
|
|
39
|
+
const retryAttempt = Symbol();
|
|
38
40
|
|
|
39
41
|
function pickCb(input) {
|
|
40
42
|
return pick(
|
|
41
43
|
input,
|
|
42
44
|
'access_token', // OAuth 2.0
|
|
43
45
|
'code', // OAuth 2.0
|
|
44
|
-
'error', // OAuth 2.0
|
|
45
46
|
'error_description', // OAuth 2.0
|
|
46
47
|
'error_uri', // OAuth 2.0
|
|
48
|
+
'error', // OAuth 2.0
|
|
47
49
|
'expires_in', // OAuth 2.0
|
|
48
50
|
'id_token', // OIDC Core 1.0
|
|
51
|
+
'iss', // draft-ietf-oauth-iss-auth-resp
|
|
52
|
+
'response', // FAPI JARM
|
|
53
|
+
'session_state', // OIDC Session Management
|
|
49
54
|
'state', // OAuth 2.0
|
|
50
55
|
'token_type', // OAuth 2.0
|
|
51
|
-
'session_state', // OIDC Session Management
|
|
52
|
-
'response', // FAPI JARM
|
|
53
56
|
);
|
|
54
57
|
}
|
|
55
58
|
|
|
@@ -396,6 +399,25 @@ class BaseClient {
|
|
|
396
399
|
});
|
|
397
400
|
}
|
|
398
401
|
|
|
402
|
+
if ('iss' in params) {
|
|
403
|
+
assertIssuerConfiguration(this.issuer, 'issuer');
|
|
404
|
+
if (params.iss !== this.issuer.issuer) {
|
|
405
|
+
throw new RPError({
|
|
406
|
+
printf: ['iss mismatch, expected %s, got: %s', this.issuer.issuer, params.iss],
|
|
407
|
+
params,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
} else if (
|
|
411
|
+
this.issuer.authorization_response_iss_parameter_supported &&
|
|
412
|
+
!('id_token' in params) &&
|
|
413
|
+
!('response' in parameters)
|
|
414
|
+
) {
|
|
415
|
+
throw new RPError({
|
|
416
|
+
message: 'iss missing from the response',
|
|
417
|
+
params,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
399
421
|
if (params.error) {
|
|
400
422
|
throw new OPError(params);
|
|
401
423
|
}
|
|
@@ -522,10 +544,37 @@ class BaseClient {
|
|
|
522
544
|
});
|
|
523
545
|
}
|
|
524
546
|
|
|
547
|
+
if ('iss' in params) {
|
|
548
|
+
assertIssuerConfiguration(this.issuer, 'issuer');
|
|
549
|
+
if (params.iss !== this.issuer.issuer) {
|
|
550
|
+
throw new RPError({
|
|
551
|
+
printf: ['iss mismatch, expected %s, got: %s', this.issuer.issuer, params.iss],
|
|
552
|
+
params,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
} else if (
|
|
556
|
+
this.issuer.authorization_response_iss_parameter_supported &&
|
|
557
|
+
!('id_token' in params) &&
|
|
558
|
+
!('response' in parameters)
|
|
559
|
+
) {
|
|
560
|
+
throw new RPError({
|
|
561
|
+
message: 'iss missing from the response',
|
|
562
|
+
params,
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
|
|
525
566
|
if (params.error) {
|
|
526
567
|
throw new OPError(params);
|
|
527
568
|
}
|
|
528
569
|
|
|
570
|
+
if ('id_token' in params) {
|
|
571
|
+
throw new RPError({
|
|
572
|
+
message:
|
|
573
|
+
'id_token detected in the response, you must use client.callback() instead of client.oauthCallback()',
|
|
574
|
+
params,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
|
|
529
578
|
const RESPONSE_TYPE_REQUIRED_PARAMS = {
|
|
530
579
|
code: ['code'],
|
|
531
580
|
token: ['access_token', 'token_type'],
|
|
@@ -569,6 +618,14 @@ class BaseClient {
|
|
|
569
618
|
{ clientAssertionPayload, DPoP },
|
|
570
619
|
);
|
|
571
620
|
|
|
621
|
+
if ('id_token' in tokenset) {
|
|
622
|
+
throw new RPError({
|
|
623
|
+
message:
|
|
624
|
+
'id_token detected in the response, you must use client.callback() instead of client.oauthCallback()',
|
|
625
|
+
params,
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
|
|
572
629
|
if (tokenset.scope && checks.scope && this.fapi()) {
|
|
573
630
|
const expected = new Set(checks.scope.split(' '));
|
|
574
631
|
const actual = tokenset.scope.split(' ');
|
|
@@ -1064,6 +1121,7 @@ class BaseClient {
|
|
|
1064
1121
|
? accessToken.token_type
|
|
1065
1122
|
: 'Bearer',
|
|
1066
1123
|
} = {},
|
|
1124
|
+
retry,
|
|
1067
1125
|
) {
|
|
1068
1126
|
if (accessToken instanceof TokenSet) {
|
|
1069
1127
|
if (!accessToken.access_token) {
|
|
@@ -1088,7 +1146,7 @@ class BaseClient {
|
|
|
1088
1146
|
|
|
1089
1147
|
const mTLS = !!this.tls_client_certificate_bound_access_tokens;
|
|
1090
1148
|
|
|
1091
|
-
|
|
1149
|
+
const response = await request.call(
|
|
1092
1150
|
this,
|
|
1093
1151
|
{
|
|
1094
1152
|
...requestOpts,
|
|
@@ -1098,6 +1156,24 @@ class BaseClient {
|
|
|
1098
1156
|
},
|
|
1099
1157
|
{ accessToken, mTLS, DPoP },
|
|
1100
1158
|
);
|
|
1159
|
+
|
|
1160
|
+
const wwwAuthenticate = response.headers['www-authenticate'];
|
|
1161
|
+
if (
|
|
1162
|
+
retry !== retryAttempt &&
|
|
1163
|
+
wwwAuthenticate &&
|
|
1164
|
+
wwwAuthenticate.toLowerCase().startsWith('dpop ') &&
|
|
1165
|
+
parseWwwAuthenticate(wwwAuthenticate).error === 'use_dpop_nonce'
|
|
1166
|
+
) {
|
|
1167
|
+
return this.requestResource(resourceUrl, accessToken, {
|
|
1168
|
+
method,
|
|
1169
|
+
headers,
|
|
1170
|
+
body,
|
|
1171
|
+
DPoP,
|
|
1172
|
+
tokenType,
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
return response;
|
|
1101
1177
|
}
|
|
1102
1178
|
|
|
1103
1179
|
async userinfo(accessToken, { method = 'GET', via = 'header', tokenType, params, DPoP } = {}) {
|
|
@@ -1243,15 +1319,10 @@ class BaseClient {
|
|
|
1243
1319
|
return this.encryptionSecret(parseInt(RegExp.$2 || RegExp.$1, 10));
|
|
1244
1320
|
}
|
|
1245
1321
|
|
|
1246
|
-
|
|
1247
|
-
Math.max(parseInt(alg.substr(-3), 10) >> 3, this.client_secret.length),
|
|
1248
|
-
);
|
|
1249
|
-
secret.write(this.client_secret);
|
|
1250
|
-
|
|
1251
|
-
return secret;
|
|
1322
|
+
return new TextEncoder().encode(this.client_secret);
|
|
1252
1323
|
}
|
|
1253
1324
|
|
|
1254
|
-
async grant(body, { clientAssertionPayload, DPoP } = {}) {
|
|
1325
|
+
async grant(body, { clientAssertionPayload, DPoP } = {}, retry) {
|
|
1255
1326
|
assertIssuerConfiguration(this.issuer, 'token_endpoint');
|
|
1256
1327
|
const response = await authenticatedPost.call(
|
|
1257
1328
|
this,
|
|
@@ -1262,7 +1333,15 @@ class BaseClient {
|
|
|
1262
1333
|
},
|
|
1263
1334
|
{ clientAssertionPayload, DPoP },
|
|
1264
1335
|
);
|
|
1265
|
-
|
|
1336
|
+
let responseBody;
|
|
1337
|
+
try {
|
|
1338
|
+
responseBody = processResponse(response);
|
|
1339
|
+
} catch (err) {
|
|
1340
|
+
if (retry !== retryAttempt && err instanceof OPError && err.error === 'use_dpop_nonce') {
|
|
1341
|
+
return this.grant(body, { clientAssertionPayload, DPoP }, retryAttempt);
|
|
1342
|
+
}
|
|
1343
|
+
throw err;
|
|
1344
|
+
}
|
|
1266
1345
|
|
|
1267
1346
|
return new TokenSet(responseBody);
|
|
1268
1347
|
}
|
|
@@ -1724,7 +1803,7 @@ Object.defineProperty(BaseClient.prototype, 'dpopProof', {
|
|
|
1724
1803
|
configurable: true,
|
|
1725
1804
|
value(...args) {
|
|
1726
1805
|
process.emitWarning(
|
|
1727
|
-
'The DPoP APIs implements an IETF draft (https://www.ietf.org/archive/id/draft-ietf-oauth-dpop-
|
|
1806
|
+
'The DPoP APIs implements an IETF draft (https://www.ietf.org/archive/id/draft-ietf-oauth-dpop-04.html). Breaking draft implementations are included as minor versions of the openid-client library, therefore, the ~ semver operator should be used and close attention be payed to library changelog as well as the drafts themselves.',
|
|
1728
1807
|
'DraftWarning',
|
|
1729
1808
|
);
|
|
1730
1809
|
Object.defineProperty(BaseClient.prototype, 'dpopProof', {
|
|
@@ -2,17 +2,10 @@ const { STATUS_CODES } = require('http');
|
|
|
2
2
|
const { format } = require('util');
|
|
3
3
|
|
|
4
4
|
const { OPError } = require('../errors');
|
|
5
|
+
const parseWwwAuthenticate = require('./www_authenticate_parser');
|
|
5
6
|
|
|
6
|
-
const REGEXP = /(\w+)=("[^"]*")/g;
|
|
7
7
|
const throwAuthenticateErrors = (response) => {
|
|
8
|
-
const params =
|
|
9
|
-
try {
|
|
10
|
-
while (REGEXP.exec(response.headers['www-authenticate']) !== null) {
|
|
11
|
-
if (RegExp.$1 && RegExp.$2) {
|
|
12
|
-
params[RegExp.$1] = RegExp.$2.slice(1, -1);
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
} catch (err) {}
|
|
8
|
+
const params = parseWwwAuthenticate(response.headers['www-authenticate']);
|
|
16
9
|
|
|
17
10
|
if (params.error) {
|
|
18
11
|
throw new OPError(params, response);
|
package/lib/helpers/request.js
CHANGED
|
@@ -4,6 +4,8 @@ const http = require('http');
|
|
|
4
4
|
const https = require('https');
|
|
5
5
|
const { once } = require('events');
|
|
6
6
|
|
|
7
|
+
const LRU = require('lru-cache');
|
|
8
|
+
|
|
7
9
|
const pkg = require('../../package.json');
|
|
8
10
|
const { RPError } = require('../errors');
|
|
9
11
|
|
|
@@ -12,6 +14,7 @@ const { deep: defaultsDeep } = require('./defaults');
|
|
|
12
14
|
const { HTTP_OPTIONS } = require('./consts');
|
|
13
15
|
|
|
14
16
|
let DEFAULT_HTTP_OPTIONS;
|
|
17
|
+
const NQCHAR = /^[\x21\x23-\x5B\x5D-\x7E]+$/;
|
|
15
18
|
|
|
16
19
|
const allowed = [
|
|
17
20
|
'agent',
|
|
@@ -52,6 +55,8 @@ function send(req, body, contentType) {
|
|
|
52
55
|
req.end();
|
|
53
56
|
}
|
|
54
57
|
|
|
58
|
+
const nonces = new LRU({ max: 100 });
|
|
59
|
+
|
|
55
60
|
module.exports = async function request(options, { accessToken, mTLS = false, DPoP } = {}) {
|
|
56
61
|
let url;
|
|
57
62
|
try {
|
|
@@ -64,12 +69,14 @@ module.exports = async function request(options, { accessToken, mTLS = false, DP
|
|
|
64
69
|
const optsFn = this[HTTP_OPTIONS];
|
|
65
70
|
let opts = options;
|
|
66
71
|
|
|
72
|
+
const nonceKey = `${url.origin}${url.pathname}`;
|
|
67
73
|
if (DPoP && 'dpopProof' in this) {
|
|
68
74
|
opts.headers = opts.headers || {};
|
|
69
75
|
opts.headers.DPoP = await this.dpopProof(
|
|
70
76
|
{
|
|
71
|
-
htu: url,
|
|
77
|
+
htu: url.href,
|
|
72
78
|
htm: options.method,
|
|
79
|
+
nonce: nonces.get(nonceKey),
|
|
73
80
|
},
|
|
74
81
|
DPoP,
|
|
75
82
|
accessToken,
|
|
@@ -173,10 +180,17 @@ module.exports = async function request(options, { accessToken, mTLS = false, DP
|
|
|
173
180
|
}
|
|
174
181
|
|
|
175
182
|
return response;
|
|
176
|
-
})()
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
183
|
+
})()
|
|
184
|
+
.catch((err) => {
|
|
185
|
+
if (response) Object.defineProperty(err, 'response', { value: response });
|
|
186
|
+
throw err;
|
|
187
|
+
})
|
|
188
|
+
.finally(() => {
|
|
189
|
+
const dpopNonce = response && response.headers['dpop-nonce'];
|
|
190
|
+
if (dpopNonce && NQCHAR.test(dpopNonce)) {
|
|
191
|
+
nonces.set(nonceKey, dpopNonce);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
180
194
|
};
|
|
181
195
|
|
|
182
196
|
module.exports.setDefaults = setDefaults.bind(undefined, allowed);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const REGEXP = /(\w+)=("[^"]*")/g;
|
|
2
|
+
|
|
3
|
+
module.exports = (wwwAuthenticate) => {
|
|
4
|
+
const params = {};
|
|
5
|
+
try {
|
|
6
|
+
while (REGEXP.exec(wwwAuthenticate) !== null) {
|
|
7
|
+
if (RegExp.$1 && RegExp.$2) {
|
|
8
|
+
params[RegExp.$1] = RegExp.$2.slice(1, -1);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
} catch (err) {}
|
|
12
|
+
|
|
13
|
+
return params;
|
|
14
|
+
};
|
package/lib/issuer.js
CHANGED
|
@@ -16,7 +16,7 @@ const AAD_MULTITENANT_DISCOVERY = [
|
|
|
16
16
|
'https://login.microsoftonline.com/organizations/v2.0/.well-known/openid-configuration',
|
|
17
17
|
'https://login.microsoftonline.com/consumers/v2.0/.well-known/openid-configuration',
|
|
18
18
|
];
|
|
19
|
-
const AAD_MULTITENANT = Symbol(
|
|
19
|
+
const AAD_MULTITENANT = Symbol();
|
|
20
20
|
const ISSUER_DEFAULTS = {
|
|
21
21
|
claim_types_supported: ['normal'],
|
|
22
22
|
claims_parameter_supported: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openid-client",
|
|
3
|
-
"version": "5.0
|
|
3
|
+
"version": "5.1.0",
|
|
4
4
|
"description": "OpenID Connect Relying Party (RP, Client) implementation for Node.js runtime, supports passportjs",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"auth",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
]
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"jose": "^4.1.
|
|
54
|
+
"jose": "^4.1.4",
|
|
55
55
|
"lru-cache": "^6.0.0",
|
|
56
56
|
"object-hash": "^2.0.1",
|
|
57
57
|
"oidc-token-hash": "^5.0.1"
|