oauth4webapi 2.17.0 → 3.0.1
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 +24 -10
- package/build/index.d.ts +800 -564
- package/build/index.js +1084 -762
- package/build/index.js.map +1 -0
- package/package.json +21 -11
package/build/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
let USER_AGENT;
|
|
2
2
|
if (typeof navigator === 'undefined' || !navigator.userAgent?.startsWith?.('Mozilla/5.0 ')) {
|
|
3
3
|
const NAME = 'oauth4webapi';
|
|
4
|
-
const VERSION = '
|
|
4
|
+
const VERSION = 'v3.0.1';
|
|
5
5
|
USER_AGENT = `${NAME}/${VERSION}`;
|
|
6
6
|
}
|
|
7
7
|
function looseInstanceOf(input, expected) {
|
|
@@ -16,13 +16,20 @@ function looseInstanceOf(input, expected) {
|
|
|
16
16
|
return false;
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
+
const ERR_INVALID_ARG_VALUE = 'ERR_INVALID_ARG_VALUE';
|
|
20
|
+
const ERR_INVALID_ARG_TYPE = 'ERR_INVALID_ARG_TYPE';
|
|
21
|
+
function CodedTypeError(message, code, cause) {
|
|
22
|
+
const err = new TypeError(message, { cause });
|
|
23
|
+
Object.assign(err, { code });
|
|
24
|
+
return err;
|
|
25
|
+
}
|
|
26
|
+
export const allowInsecureRequests = Symbol();
|
|
19
27
|
export const clockSkew = Symbol();
|
|
20
28
|
export const clockTolerance = Symbol();
|
|
21
29
|
export const customFetch = Symbol();
|
|
22
30
|
export const modifyAssertion = Symbol();
|
|
23
31
|
export const jweDecrypt = Symbol();
|
|
24
32
|
export const jwksCache = Symbol();
|
|
25
|
-
export const useMtlsAlias = Symbol();
|
|
26
33
|
const encoder = new TextEncoder();
|
|
27
34
|
const decoder = new TextDecoder();
|
|
28
35
|
function buf(input) {
|
|
@@ -52,7 +59,7 @@ function decodeBase64Url(input) {
|
|
|
52
59
|
return bytes;
|
|
53
60
|
}
|
|
54
61
|
catch (cause) {
|
|
55
|
-
throw
|
|
62
|
+
throw CodedTypeError('The input to be decoded is not correctly encoded.', ERR_INVALID_ARG_VALUE, cause);
|
|
56
63
|
}
|
|
57
64
|
}
|
|
58
65
|
function b64u(input) {
|
|
@@ -61,98 +68,45 @@ function b64u(input) {
|
|
|
61
68
|
}
|
|
62
69
|
return encodeBase64Url(input);
|
|
63
70
|
}
|
|
64
|
-
class LRU {
|
|
65
|
-
constructor(maxSize) {
|
|
66
|
-
this.cache = new Map();
|
|
67
|
-
this._cache = new Map();
|
|
68
|
-
this.maxSize = maxSize;
|
|
69
|
-
}
|
|
70
|
-
get(key) {
|
|
71
|
-
let v = this.cache.get(key);
|
|
72
|
-
if (v) {
|
|
73
|
-
return v;
|
|
74
|
-
}
|
|
75
|
-
if ((v = this._cache.get(key))) {
|
|
76
|
-
this.update(key, v);
|
|
77
|
-
return v;
|
|
78
|
-
}
|
|
79
|
-
return undefined;
|
|
80
|
-
}
|
|
81
|
-
has(key) {
|
|
82
|
-
return this.cache.has(key) || this._cache.has(key);
|
|
83
|
-
}
|
|
84
|
-
set(key, value) {
|
|
85
|
-
if (this.cache.has(key)) {
|
|
86
|
-
this.cache.set(key, value);
|
|
87
|
-
}
|
|
88
|
-
else {
|
|
89
|
-
this.update(key, value);
|
|
90
|
-
}
|
|
91
|
-
return this;
|
|
92
|
-
}
|
|
93
|
-
delete(key) {
|
|
94
|
-
if (this.cache.has(key)) {
|
|
95
|
-
return this.cache.delete(key);
|
|
96
|
-
}
|
|
97
|
-
if (this._cache.has(key)) {
|
|
98
|
-
return this._cache.delete(key);
|
|
99
|
-
}
|
|
100
|
-
return false;
|
|
101
|
-
}
|
|
102
|
-
update(key, value) {
|
|
103
|
-
this.cache.set(key, value);
|
|
104
|
-
if (this.cache.size >= this.maxSize) {
|
|
105
|
-
this._cache = this.cache;
|
|
106
|
-
this.cache = new Map();
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
71
|
export class UnsupportedOperationError extends Error {
|
|
111
|
-
|
|
112
|
-
|
|
72
|
+
code;
|
|
73
|
+
constructor(message, options) {
|
|
74
|
+
super(message, options);
|
|
113
75
|
this.name = this.constructor.name;
|
|
76
|
+
this.code = UNSUPPORTED_OPERATION;
|
|
114
77
|
Error.captureStackTrace?.(this, this.constructor);
|
|
115
78
|
}
|
|
116
79
|
}
|
|
117
80
|
export class OperationProcessingError extends Error {
|
|
81
|
+
code;
|
|
118
82
|
constructor(message, options) {
|
|
119
83
|
super(message, options);
|
|
120
84
|
this.name = this.constructor.name;
|
|
85
|
+
if (options?.code) {
|
|
86
|
+
this.code = options?.code;
|
|
87
|
+
}
|
|
121
88
|
Error.captureStackTrace?.(this, this.constructor);
|
|
122
89
|
}
|
|
123
90
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
function
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
'
|
|
141
|
-
|
|
142
|
-
'PS512',
|
|
143
|
-
'ES512',
|
|
144
|
-
'RS512',
|
|
145
|
-
'EdDSA',
|
|
146
|
-
];
|
|
147
|
-
function processDpopNonce(response) {
|
|
148
|
-
try {
|
|
149
|
-
const nonce = response.headers.get('dpop-nonce');
|
|
150
|
-
if (nonce) {
|
|
151
|
-
dpopNonces.set(new URL(response.url).origin, nonce);
|
|
152
|
-
}
|
|
91
|
+
function OPE(message, code, cause) {
|
|
92
|
+
return new OperationProcessingError(message, { code, cause });
|
|
93
|
+
}
|
|
94
|
+
function assertCryptoKey(key, it) {
|
|
95
|
+
if (!(key instanceof CryptoKey)) {
|
|
96
|
+
throw CodedTypeError(`${it} must be a CryptoKey`, ERR_INVALID_ARG_TYPE);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function assertPrivateKey(key, it) {
|
|
100
|
+
assertCryptoKey(key, it);
|
|
101
|
+
if (key.type !== 'private') {
|
|
102
|
+
throw CodedTypeError(`${it} must be a private CryptoKey`, ERR_INVALID_ARG_VALUE);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function assertPublicKey(key, it) {
|
|
106
|
+
assertCryptoKey(key, it);
|
|
107
|
+
if (key.type !== 'public') {
|
|
108
|
+
throw CodedTypeError(`${it} must be a public CryptoKey`, ERR_INVALID_ARG_VALUE);
|
|
153
109
|
}
|
|
154
|
-
catch { }
|
|
155
|
-
return response;
|
|
156
110
|
}
|
|
157
111
|
function normalizeTyp(value) {
|
|
158
112
|
return value.toLowerCase().replace(/^application\//, '');
|
|
@@ -172,10 +126,10 @@ function prepareHeaders(input) {
|
|
|
172
126
|
headers.set('user-agent', USER_AGENT);
|
|
173
127
|
}
|
|
174
128
|
if (headers.has('authorization')) {
|
|
175
|
-
throw
|
|
129
|
+
throw CodedTypeError('"options.headers" must not include the "authorization" header name', ERR_INVALID_ARG_VALUE);
|
|
176
130
|
}
|
|
177
131
|
if (headers.has('dpop')) {
|
|
178
|
-
throw
|
|
132
|
+
throw CodedTypeError('"options.headers" must not include the "dpop" header name', ERR_INVALID_ARG_VALUE);
|
|
179
133
|
}
|
|
180
134
|
return headers;
|
|
181
135
|
}
|
|
@@ -184,17 +138,15 @@ function signal(value) {
|
|
|
184
138
|
value = value();
|
|
185
139
|
}
|
|
186
140
|
if (!(value instanceof AbortSignal)) {
|
|
187
|
-
throw
|
|
141
|
+
throw CodedTypeError('"options.signal" must return or be an instance of AbortSignal', ERR_INVALID_ARG_TYPE);
|
|
188
142
|
}
|
|
189
143
|
return value;
|
|
190
144
|
}
|
|
191
145
|
export async function discoveryRequest(issuerIdentifier, options) {
|
|
192
146
|
if (!(issuerIdentifier instanceof URL)) {
|
|
193
|
-
throw
|
|
194
|
-
}
|
|
195
|
-
if (issuerIdentifier.protocol !== 'https:' && issuerIdentifier.protocol !== 'http:') {
|
|
196
|
-
throw new TypeError('"issuer.protocol" must be "https:" or "http:"');
|
|
147
|
+
throw CodedTypeError('"issuerIdentifier" must be an instance of URL', ERR_INVALID_ARG_TYPE);
|
|
197
148
|
}
|
|
149
|
+
checkProtocol(issuerIdentifier, options?.[allowInsecureRequests] !== true);
|
|
198
150
|
const url = new URL(issuerIdentifier.href);
|
|
199
151
|
switch (options?.algorithm) {
|
|
200
152
|
case undefined:
|
|
@@ -210,49 +162,110 @@ export async function discoveryRequest(issuerIdentifier, options) {
|
|
|
210
162
|
}
|
|
211
163
|
break;
|
|
212
164
|
default:
|
|
213
|
-
throw
|
|
165
|
+
throw CodedTypeError('"options.algorithm" must be "oidc" (default), or "oauth2"', ERR_INVALID_ARG_VALUE);
|
|
214
166
|
}
|
|
215
167
|
const headers = prepareHeaders(options?.headers);
|
|
216
168
|
headers.set('accept', 'application/json');
|
|
217
169
|
return (options?.[customFetch] || fetch)(url.href, {
|
|
170
|
+
body: undefined,
|
|
218
171
|
headers: Object.fromEntries(headers.entries()),
|
|
219
172
|
method: 'GET',
|
|
220
173
|
redirect: 'manual',
|
|
221
|
-
signal: options?.signal ? signal(options.signal) :
|
|
222
|
-
})
|
|
174
|
+
signal: options?.signal ? signal(options.signal) : undefined,
|
|
175
|
+
});
|
|
223
176
|
}
|
|
224
|
-
function
|
|
225
|
-
|
|
177
|
+
function assertNumber(input, allow0, it, code, cause) {
|
|
178
|
+
try {
|
|
179
|
+
if (typeof input !== 'number' || !Number.isFinite(input)) {
|
|
180
|
+
throw CodedTypeError(`${it} must be a number`, ERR_INVALID_ARG_TYPE, cause);
|
|
181
|
+
}
|
|
182
|
+
if (input > 0)
|
|
183
|
+
return;
|
|
184
|
+
if (allow0 && input !== 0) {
|
|
185
|
+
throw CodedTypeError(`${it} must be a non-negative number`, ERR_INVALID_ARG_VALUE, cause);
|
|
186
|
+
}
|
|
187
|
+
throw CodedTypeError(`${it} must be a positive number`, ERR_INVALID_ARG_VALUE, cause);
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
if (code) {
|
|
191
|
+
throw OPE(err.message, code, cause);
|
|
192
|
+
}
|
|
193
|
+
throw err;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function assertString(input, it, code, cause) {
|
|
197
|
+
try {
|
|
198
|
+
if (typeof input !== 'string') {
|
|
199
|
+
throw CodedTypeError(`${it} must be a string`, ERR_INVALID_ARG_TYPE, cause);
|
|
200
|
+
}
|
|
201
|
+
if (input.length === 0) {
|
|
202
|
+
throw CodedTypeError(`${it} must not be empty`, ERR_INVALID_ARG_VALUE, cause);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
if (code) {
|
|
207
|
+
throw OPE(err.message, code, cause);
|
|
208
|
+
}
|
|
209
|
+
throw err;
|
|
210
|
+
}
|
|
226
211
|
}
|
|
227
212
|
export async function processDiscoveryResponse(expectedIssuerIdentifier, response) {
|
|
228
|
-
if (!(expectedIssuerIdentifier instanceof URL)
|
|
229
|
-
|
|
213
|
+
if (!(expectedIssuerIdentifier instanceof URL) &&
|
|
214
|
+
expectedIssuerIdentifier !== _nodiscoverycheck) {
|
|
215
|
+
throw CodedTypeError('"expectedIssuer" must be an instance of URL', ERR_INVALID_ARG_TYPE);
|
|
230
216
|
}
|
|
231
217
|
if (!looseInstanceOf(response, Response)) {
|
|
232
|
-
throw
|
|
218
|
+
throw CodedTypeError('"response" must be an instance of Response', ERR_INVALID_ARG_TYPE);
|
|
233
219
|
}
|
|
234
220
|
if (response.status !== 200) {
|
|
235
|
-
throw
|
|
221
|
+
throw OPE('"response" is not a conform Authorization Server Metadata response', RESPONSE_IS_NOT_CONFORM, response);
|
|
236
222
|
}
|
|
237
223
|
assertReadableResponse(response);
|
|
224
|
+
assertApplicationJson(response);
|
|
238
225
|
let json;
|
|
239
226
|
try {
|
|
240
227
|
json = await response.json();
|
|
241
228
|
}
|
|
242
229
|
catch (cause) {
|
|
243
|
-
throw
|
|
230
|
+
throw OPE('failed to parse "response" body as JSON', PARSE_ERROR, cause);
|
|
244
231
|
}
|
|
245
232
|
if (!isJsonObject(json)) {
|
|
246
|
-
throw
|
|
247
|
-
}
|
|
248
|
-
if (!validateString(json.issuer)) {
|
|
249
|
-
throw new OPE('"response" body "issuer" property must be a non-empty string');
|
|
233
|
+
throw OPE('"response" body must be a top level object', INVALID_RESPONSE, { body: json });
|
|
250
234
|
}
|
|
251
|
-
|
|
252
|
-
|
|
235
|
+
assertString(json.issuer, '"response" body "issuer" property', INVALID_RESPONSE, { body: json });
|
|
236
|
+
if (new URL(json.issuer).href !== expectedIssuerIdentifier.href &&
|
|
237
|
+
expectedIssuerIdentifier !== _nodiscoverycheck) {
|
|
238
|
+
throw OPE('"response" body "issuer" property does not match the expected value', JSON_ATTRIBUTE_COMPARISON, { expected: expectedIssuerIdentifier.href, body: json, attribute: 'issuer' });
|
|
253
239
|
}
|
|
254
240
|
return json;
|
|
255
241
|
}
|
|
242
|
+
function assertApplicationJson(response) {
|
|
243
|
+
assertContentType(response, 'application/json');
|
|
244
|
+
}
|
|
245
|
+
function notJson(response, ...types) {
|
|
246
|
+
let msg = '"response" content-type must be ';
|
|
247
|
+
if (types.length > 2) {
|
|
248
|
+
const last = types.pop();
|
|
249
|
+
msg += `${types.join(', ')}, or ${last}`;
|
|
250
|
+
}
|
|
251
|
+
else if (types.length === 2) {
|
|
252
|
+
msg += `${types[0]} or ${types[1]}`;
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
msg += types[0];
|
|
256
|
+
}
|
|
257
|
+
return OPE(msg, RESPONSE_IS_NOT_JSON, response);
|
|
258
|
+
}
|
|
259
|
+
function assertContentTypes(response, ...types) {
|
|
260
|
+
if (!types.includes(getContentType(response))) {
|
|
261
|
+
throw notJson(response, ...types);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function assertContentType(response, contentType) {
|
|
265
|
+
if (getContentType(response) !== contentType) {
|
|
266
|
+
throw notJson(response, contentType);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
256
269
|
function randomBytes() {
|
|
257
270
|
return b64u(crypto.getRandomValues(new Uint8Array(32)));
|
|
258
271
|
}
|
|
@@ -266,9 +279,7 @@ export function generateRandomNonce() {
|
|
|
266
279
|
return randomBytes();
|
|
267
280
|
}
|
|
268
281
|
export async function calculatePKCECodeChallenge(codeVerifier) {
|
|
269
|
-
|
|
270
|
-
throw new TypeError('"codeVerifier" must be a non-empty string');
|
|
271
|
-
}
|
|
282
|
+
assertString(codeVerifier, 'codeVerifier');
|
|
272
283
|
return b64u(await crypto.subtle.digest('SHA-256', buf(codeVerifier)));
|
|
273
284
|
}
|
|
274
285
|
function getKeyAndKid(input) {
|
|
@@ -278,24 +289,14 @@ function getKeyAndKid(input) {
|
|
|
278
289
|
if (!(input?.key instanceof CryptoKey)) {
|
|
279
290
|
return {};
|
|
280
291
|
}
|
|
281
|
-
if (input.kid !== undefined
|
|
282
|
-
|
|
292
|
+
if (input.kid !== undefined) {
|
|
293
|
+
assertString(input.kid, '"kid"');
|
|
283
294
|
}
|
|
284
295
|
return {
|
|
285
296
|
key: input.key,
|
|
286
297
|
kid: input.kid,
|
|
287
|
-
modifyAssertion: input[modifyAssertion],
|
|
288
298
|
};
|
|
289
299
|
}
|
|
290
|
-
function formUrlEncode(token) {
|
|
291
|
-
return encodeURIComponent(token).replace(/%20/g, '+');
|
|
292
|
-
}
|
|
293
|
-
function clientSecretBasic(clientId, clientSecret) {
|
|
294
|
-
const username = formUrlEncode(clientId);
|
|
295
|
-
const password = formUrlEncode(clientSecret);
|
|
296
|
-
const credentials = btoa(`${username}:${password}`);
|
|
297
|
-
return `Basic ${credentials}`;
|
|
298
|
-
}
|
|
299
300
|
function psAlg(key) {
|
|
300
301
|
switch (key.algorithm.hash.name) {
|
|
301
302
|
case 'SHA-256':
|
|
@@ -305,7 +306,9 @@ function psAlg(key) {
|
|
|
305
306
|
case 'SHA-512':
|
|
306
307
|
return 'PS512';
|
|
307
308
|
default:
|
|
308
|
-
throw new UnsupportedOperationError('unsupported RsaHashedKeyAlgorithm hash name'
|
|
309
|
+
throw new UnsupportedOperationError('unsupported RsaHashedKeyAlgorithm hash name', {
|
|
310
|
+
cause: key,
|
|
311
|
+
});
|
|
309
312
|
}
|
|
310
313
|
}
|
|
311
314
|
function rsAlg(key) {
|
|
@@ -317,7 +320,9 @@ function rsAlg(key) {
|
|
|
317
320
|
case 'SHA-512':
|
|
318
321
|
return 'RS512';
|
|
319
322
|
default:
|
|
320
|
-
throw new UnsupportedOperationError('unsupported RsaHashedKeyAlgorithm hash name'
|
|
323
|
+
throw new UnsupportedOperationError('unsupported RsaHashedKeyAlgorithm hash name', {
|
|
324
|
+
cause: key,
|
|
325
|
+
});
|
|
321
326
|
}
|
|
322
327
|
}
|
|
323
328
|
function esAlg(key) {
|
|
@@ -329,7 +334,7 @@ function esAlg(key) {
|
|
|
329
334
|
case 'P-521':
|
|
330
335
|
return 'ES512';
|
|
331
336
|
default:
|
|
332
|
-
throw new UnsupportedOperationError('unsupported EcKeyAlgorithm namedCurve');
|
|
337
|
+
throw new UnsupportedOperationError('unsupported EcKeyAlgorithm namedCurve', { cause: key });
|
|
333
338
|
}
|
|
334
339
|
}
|
|
335
340
|
function keyToJws(key) {
|
|
@@ -341,10 +346,10 @@ function keyToJws(key) {
|
|
|
341
346
|
case 'ECDSA':
|
|
342
347
|
return esAlg(key);
|
|
343
348
|
case 'Ed25519':
|
|
344
|
-
case '
|
|
345
|
-
return '
|
|
349
|
+
case 'EdDSA':
|
|
350
|
+
return 'Ed25519';
|
|
346
351
|
default:
|
|
347
|
-
throw new UnsupportedOperationError('unsupported CryptoKey algorithm name');
|
|
352
|
+
throw new UnsupportedOperationError('unsupported CryptoKey algorithm name', { cause: key });
|
|
348
353
|
}
|
|
349
354
|
}
|
|
350
355
|
function getClockSkew(client) {
|
|
@@ -360,11 +365,59 @@ function getClockTolerance(client) {
|
|
|
360
365
|
function epochTime() {
|
|
361
366
|
return Math.floor(Date.now() / 1000);
|
|
362
367
|
}
|
|
363
|
-
function
|
|
368
|
+
function assertAs(as) {
|
|
369
|
+
if (typeof as !== 'object' || as === null) {
|
|
370
|
+
throw CodedTypeError('"as" must be an object', ERR_INVALID_ARG_TYPE);
|
|
371
|
+
}
|
|
372
|
+
assertString(as.issuer, '"as.issuer"');
|
|
373
|
+
}
|
|
374
|
+
function assertClient(client) {
|
|
375
|
+
if (typeof client !== 'object' || client === null) {
|
|
376
|
+
throw CodedTypeError('"client" must be an object', ERR_INVALID_ARG_TYPE);
|
|
377
|
+
}
|
|
378
|
+
assertString(client.client_id, '"client.client_id"');
|
|
379
|
+
}
|
|
380
|
+
function formUrlEncode(token) {
|
|
381
|
+
return encodeURIComponent(token).replace(/(?:[-_.!~*'()]|%20)/g, (substring) => {
|
|
382
|
+
switch (substring) {
|
|
383
|
+
case '-':
|
|
384
|
+
case '_':
|
|
385
|
+
case '.':
|
|
386
|
+
case '!':
|
|
387
|
+
case '~':
|
|
388
|
+
case '*':
|
|
389
|
+
case "'":
|
|
390
|
+
case '(':
|
|
391
|
+
case ')':
|
|
392
|
+
return `%${substring.charCodeAt(0).toString(16).toUpperCase()}`;
|
|
393
|
+
case '%20':
|
|
394
|
+
return '+';
|
|
395
|
+
default:
|
|
396
|
+
throw new Error();
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
export function ClientSecretPost(clientSecret) {
|
|
401
|
+
assertString(clientSecret, '"clientSecret"');
|
|
402
|
+
return (_as, client, body, _headers) => {
|
|
403
|
+
body.set('client_id', client.client_id);
|
|
404
|
+
body.set('client_secret', clientSecret);
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
export function ClientSecretBasic(clientSecret) {
|
|
408
|
+
assertString(clientSecret, '"clientSecret"');
|
|
409
|
+
return (_as, client, _body, headers) => {
|
|
410
|
+
const username = formUrlEncode(client.client_id);
|
|
411
|
+
const password = formUrlEncode(clientSecret);
|
|
412
|
+
const credentials = btoa(`${username}:${password}`);
|
|
413
|
+
headers.set('authorization', `Basic ${credentials}`);
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
function clientAssertionPayload(as, client) {
|
|
364
417
|
const now = epochTime() + getClockSkew(client);
|
|
365
418
|
return {
|
|
366
419
|
jti: randomBytes(),
|
|
367
|
-
aud:
|
|
420
|
+
aud: as.issuer,
|
|
368
421
|
exp: now + 60,
|
|
369
422
|
iat: now,
|
|
370
423
|
nbf: now,
|
|
@@ -372,105 +425,56 @@ function clientAssertion(as, client) {
|
|
|
372
425
|
sub: client.client_id,
|
|
373
426
|
};
|
|
374
427
|
}
|
|
375
|
-
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
throw new TypeError('"as.issuer" property must be a non-empty string');
|
|
387
|
-
}
|
|
388
|
-
return true;
|
|
389
|
-
}
|
|
390
|
-
function assertClient(client) {
|
|
391
|
-
if (typeof client !== 'object' || client === null) {
|
|
392
|
-
throw new TypeError('"client" must be an object');
|
|
393
|
-
}
|
|
394
|
-
if (!validateString(client.client_id)) {
|
|
395
|
-
throw new TypeError('"client.client_id" property must be a non-empty string');
|
|
396
|
-
}
|
|
397
|
-
return true;
|
|
398
|
-
}
|
|
399
|
-
function assertClientSecret(clientSecret) {
|
|
400
|
-
if (!validateString(clientSecret)) {
|
|
401
|
-
throw new TypeError('"client.client_secret" property must be a non-empty string');
|
|
402
|
-
}
|
|
403
|
-
return clientSecret;
|
|
428
|
+
export function PrivateKeyJwt(clientPrivateKey, options) {
|
|
429
|
+
const { key, kid } = getKeyAndKid(clientPrivateKey);
|
|
430
|
+
assertPrivateKey(key, '"clientPrivateKey.key"');
|
|
431
|
+
return async (as, client, body, _headers) => {
|
|
432
|
+
const header = { alg: keyToJws(key), kid };
|
|
433
|
+
const payload = clientAssertionPayload(as, client);
|
|
434
|
+
options?.[modifyAssertion]?.(header, payload);
|
|
435
|
+
body.set('client_id', client.client_id);
|
|
436
|
+
body.set('client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');
|
|
437
|
+
body.set('client_assertion', await signJwt(header, payload, key));
|
|
438
|
+
};
|
|
404
439
|
}
|
|
405
|
-
function
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
440
|
+
export function ClientSecretJwt(clientSecret, options) {
|
|
441
|
+
assertString(clientSecret, '"clientSecret"');
|
|
442
|
+
const modify = options?.[modifyAssertion];
|
|
443
|
+
let key;
|
|
444
|
+
return async (as, client, body, _headers) => {
|
|
445
|
+
key ||= await crypto.subtle.importKey('raw', buf(clientSecret), { hash: 'SHA-256', name: 'HMAC' }, false, ['sign']);
|
|
446
|
+
const header = { alg: 'HS256' };
|
|
447
|
+
const payload = clientAssertionPayload(as, client);
|
|
448
|
+
modify?.(header, payload);
|
|
449
|
+
const data = `${b64u(buf(JSON.stringify(header)))}.${b64u(buf(JSON.stringify(payload)))}`;
|
|
450
|
+
const hmac = await crypto.subtle.sign(key.algorithm, key, buf(data));
|
|
451
|
+
body.set('client_id', client.client_id);
|
|
452
|
+
body.set('client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');
|
|
453
|
+
body.set('client_assertion', `${data}.${b64u(new Uint8Array(hmac))}`);
|
|
454
|
+
};
|
|
409
455
|
}
|
|
410
|
-
function
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
}
|
|
456
|
+
export function None() {
|
|
457
|
+
return (_as, client, body, _headers) => {
|
|
458
|
+
body.set('client_id', client.client_id);
|
|
459
|
+
};
|
|
414
460
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
body.delete('client_assertion_type');
|
|
418
|
-
body.delete('client_assertion');
|
|
419
|
-
switch (client.token_endpoint_auth_method) {
|
|
420
|
-
case undefined:
|
|
421
|
-
case 'client_secret_basic': {
|
|
422
|
-
assertNoClientPrivateKey('client_secret_basic', clientPrivateKey);
|
|
423
|
-
headers.set('authorization', clientSecretBasic(client.client_id, assertClientSecret(client.client_secret)));
|
|
424
|
-
break;
|
|
425
|
-
}
|
|
426
|
-
case 'client_secret_post': {
|
|
427
|
-
assertNoClientPrivateKey('client_secret_post', clientPrivateKey);
|
|
428
|
-
body.set('client_id', client.client_id);
|
|
429
|
-
body.set('client_secret', assertClientSecret(client.client_secret));
|
|
430
|
-
break;
|
|
431
|
-
}
|
|
432
|
-
case 'private_key_jwt': {
|
|
433
|
-
assertNoClientSecret('private_key_jwt', client.client_secret);
|
|
434
|
-
if (clientPrivateKey === undefined) {
|
|
435
|
-
throw new TypeError('"options.clientPrivateKey" must be provided when "client.token_endpoint_auth_method" is "private_key_jwt"');
|
|
436
|
-
}
|
|
437
|
-
const { key, kid, modifyAssertion } = getKeyAndKid(clientPrivateKey);
|
|
438
|
-
if (!isPrivateKey(key)) {
|
|
439
|
-
throw new TypeError('"options.clientPrivateKey.key" must be a private CryptoKey');
|
|
440
|
-
}
|
|
441
|
-
body.set('client_id', client.client_id);
|
|
442
|
-
body.set('client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');
|
|
443
|
-
body.set('client_assertion', await privateKeyJwt(as, client, key, kid, modifyAssertion));
|
|
444
|
-
break;
|
|
445
|
-
}
|
|
446
|
-
case 'tls_client_auth':
|
|
447
|
-
case 'self_signed_tls_client_auth':
|
|
448
|
-
case 'none': {
|
|
449
|
-
assertNoClientSecret(client.token_endpoint_auth_method, client.client_secret);
|
|
450
|
-
assertNoClientPrivateKey(client.token_endpoint_auth_method, clientPrivateKey);
|
|
451
|
-
body.set('client_id', client.client_id);
|
|
452
|
-
break;
|
|
453
|
-
}
|
|
454
|
-
default:
|
|
455
|
-
throw new UnsupportedOperationError('unsupported client token_endpoint_auth_method');
|
|
456
|
-
}
|
|
461
|
+
export function TlsClientAuth() {
|
|
462
|
+
return None();
|
|
457
463
|
}
|
|
458
|
-
async function
|
|
464
|
+
async function signJwt(header, payload, key) {
|
|
459
465
|
if (!key.usages.includes('sign')) {
|
|
460
|
-
throw
|
|
466
|
+
throw CodedTypeError('CryptoKey instances used for signing assertions must include "sign" in their "usages"', ERR_INVALID_ARG_VALUE);
|
|
461
467
|
}
|
|
462
468
|
const input = `${b64u(buf(JSON.stringify(header)))}.${b64u(buf(JSON.stringify(payload)))}`;
|
|
463
469
|
const signature = b64u(await crypto.subtle.sign(keyToSubtle(key), key, buf(input)));
|
|
464
470
|
return `${input}.${signature}`;
|
|
465
471
|
}
|
|
466
|
-
export async function issueRequestObject(as, client, parameters, privateKey) {
|
|
472
|
+
export async function issueRequestObject(as, client, parameters, privateKey, options) {
|
|
467
473
|
assertAs(as);
|
|
468
474
|
assertClient(client);
|
|
469
475
|
parameters = new URLSearchParams(parameters);
|
|
470
|
-
const { key, kid
|
|
471
|
-
|
|
472
|
-
throw new TypeError('"privateKey.key" must be a private CryptoKey');
|
|
473
|
-
}
|
|
476
|
+
const { key, kid } = getKeyAndKid(privateKey);
|
|
477
|
+
assertPrivateKey(key, '"privateKey.key"');
|
|
474
478
|
parameters.set('client_id', client.client_id);
|
|
475
479
|
const now = epochTime() + getClockSkew(client);
|
|
476
480
|
const claims = {
|
|
@@ -492,9 +496,7 @@ export async function issueRequestObject(as, client, parameters, privateKey) {
|
|
|
492
496
|
let value = parameters.get('max_age');
|
|
493
497
|
if (value !== null) {
|
|
494
498
|
claims.max_age = parseInt(value, 10);
|
|
495
|
-
|
|
496
|
-
throw new OPE('"max_age" parameter must be a number');
|
|
497
|
-
}
|
|
499
|
+
assertNumber(claims.max_age, true, '"max_age" parameter');
|
|
498
500
|
}
|
|
499
501
|
}
|
|
500
502
|
{
|
|
@@ -504,10 +506,10 @@ export async function issueRequestObject(as, client, parameters, privateKey) {
|
|
|
504
506
|
claims.claims = JSON.parse(value);
|
|
505
507
|
}
|
|
506
508
|
catch (cause) {
|
|
507
|
-
throw
|
|
509
|
+
throw OPE('failed to parse the "claims" parameter as JSON', PARSE_ERROR, cause);
|
|
508
510
|
}
|
|
509
511
|
if (!isJsonObject(claims.claims)) {
|
|
510
|
-
throw
|
|
512
|
+
throw CodedTypeError('"claims" parameter must be a JSON with a top level object', ERR_INVALID_ARG_VALUE);
|
|
511
513
|
}
|
|
512
514
|
}
|
|
513
515
|
}
|
|
@@ -518,10 +520,10 @@ export async function issueRequestObject(as, client, parameters, privateKey) {
|
|
|
518
520
|
claims.authorization_details = JSON.parse(value);
|
|
519
521
|
}
|
|
520
522
|
catch (cause) {
|
|
521
|
-
throw
|
|
523
|
+
throw OPE('failed to parse the "authorization_details" parameter as JSON', PARSE_ERROR, cause);
|
|
522
524
|
}
|
|
523
525
|
if (!Array.isArray(claims.authorization_details)) {
|
|
524
|
-
throw
|
|
526
|
+
throw CodedTypeError('"authorization_details" parameter must be a JSON with a top level array', ERR_INVALID_ARG_VALUE);
|
|
525
527
|
}
|
|
526
528
|
}
|
|
527
529
|
}
|
|
@@ -530,39 +532,8 @@ export async function issueRequestObject(as, client, parameters, privateKey) {
|
|
|
530
532
|
typ: 'oauth-authz-req+jwt',
|
|
531
533
|
kid,
|
|
532
534
|
};
|
|
533
|
-
modifyAssertion?.(header, claims);
|
|
534
|
-
return
|
|
535
|
-
}
|
|
536
|
-
async function dpopProofJwt(headers, options, url, htm, clockSkew, accessToken) {
|
|
537
|
-
const { privateKey, publicKey, nonce = dpopNonces.get(url.origin) } = options;
|
|
538
|
-
if (!isPrivateKey(privateKey)) {
|
|
539
|
-
throw new TypeError('"DPoP.privateKey" must be a private CryptoKey');
|
|
540
|
-
}
|
|
541
|
-
if (!isPublicKey(publicKey)) {
|
|
542
|
-
throw new TypeError('"DPoP.publicKey" must be a public CryptoKey');
|
|
543
|
-
}
|
|
544
|
-
if (nonce !== undefined && !validateString(nonce)) {
|
|
545
|
-
throw new TypeError('"DPoP.nonce" must be a non-empty string or undefined');
|
|
546
|
-
}
|
|
547
|
-
if (!publicKey.extractable) {
|
|
548
|
-
throw new TypeError('"DPoP.publicKey.extractable" must be true');
|
|
549
|
-
}
|
|
550
|
-
const now = epochTime() + clockSkew;
|
|
551
|
-
const header = {
|
|
552
|
-
alg: keyToJws(privateKey),
|
|
553
|
-
typ: 'dpop+jwt',
|
|
554
|
-
jwk: await publicJwk(publicKey),
|
|
555
|
-
};
|
|
556
|
-
const payload = {
|
|
557
|
-
iat: now,
|
|
558
|
-
jti: randomBytes(),
|
|
559
|
-
htm,
|
|
560
|
-
nonce,
|
|
561
|
-
htu: `${url.origin}${url.pathname}`,
|
|
562
|
-
ath: accessToken ? b64u(await crypto.subtle.digest('SHA-256', buf(accessToken))) : undefined,
|
|
563
|
-
};
|
|
564
|
-
options[modifyAssertion]?.(header, payload);
|
|
565
|
-
headers.set('dpop', await jwt(header, payload, privateKey));
|
|
535
|
+
options?.[modifyAssertion]?.(header, claims);
|
|
536
|
+
return signJwt(header, claims, key);
|
|
566
537
|
}
|
|
567
538
|
let jwkCache;
|
|
568
539
|
async function getSetPublicJwkCache(key) {
|
|
@@ -572,49 +543,185 @@ async function getSetPublicJwkCache(key) {
|
|
|
572
543
|
return jwk;
|
|
573
544
|
}
|
|
574
545
|
async function publicJwk(key) {
|
|
575
|
-
jwkCache
|
|
546
|
+
jwkCache ||= new WeakMap();
|
|
576
547
|
return jwkCache.get(key) || getSetPublicJwkCache(key);
|
|
577
548
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
549
|
+
const URLParse = URL.parse
|
|
550
|
+
?
|
|
551
|
+
(url, base) => URL.parse(url, base)
|
|
552
|
+
: (url, base) => {
|
|
553
|
+
try {
|
|
554
|
+
return new URL(url, base);
|
|
582
555
|
}
|
|
583
|
-
|
|
556
|
+
catch {
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
export function checkProtocol(url, enforceHttps) {
|
|
561
|
+
if (enforceHttps && url.protocol !== 'https:') {
|
|
562
|
+
throw OPE('only requests to HTTPS are allowed', HTTP_REQUEST_FORBIDDEN, url);
|
|
563
|
+
}
|
|
564
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
|
|
565
|
+
throw OPE('only HTTP and HTTPS requests are allowed', REQUEST_PROTOCOL_FORBIDDEN, url);
|
|
584
566
|
}
|
|
585
|
-
return new URL(value);
|
|
586
567
|
}
|
|
587
|
-
function
|
|
588
|
-
|
|
589
|
-
|
|
568
|
+
function validateEndpoint(value, endpoint, useMtlsAlias, enforceHttps) {
|
|
569
|
+
let url;
|
|
570
|
+
if (typeof value !== 'string' || !(url = URLParse(value))) {
|
|
571
|
+
throw OPE(`authorization server metadata does not contain a valid ${useMtlsAlias ? `"as.mtls_endpoint_aliases.${endpoint}"` : `"as.${endpoint}"`}`, value === undefined ? MISSING_SERVER_METADATA : INVALID_SERVER_METADATA, { attribute: useMtlsAlias ? `mtls_endpoint_aliases.${endpoint}` : endpoint });
|
|
590
572
|
}
|
|
591
|
-
|
|
573
|
+
checkProtocol(url, enforceHttps);
|
|
574
|
+
return url;
|
|
592
575
|
}
|
|
593
|
-
function
|
|
594
|
-
if (
|
|
595
|
-
return
|
|
576
|
+
export function resolveEndpoint(as, endpoint, useMtlsAlias, enforceHttps) {
|
|
577
|
+
if (useMtlsAlias && as.mtls_endpoint_aliases && endpoint in as.mtls_endpoint_aliases) {
|
|
578
|
+
return validateEndpoint(as.mtls_endpoint_aliases[endpoint], endpoint, useMtlsAlias, enforceHttps);
|
|
596
579
|
}
|
|
597
|
-
return
|
|
580
|
+
return validateEndpoint(as[endpoint], endpoint, useMtlsAlias, enforceHttps);
|
|
598
581
|
}
|
|
599
|
-
export async function pushedAuthorizationRequest(as, client, parameters, options) {
|
|
582
|
+
export async function pushedAuthorizationRequest(as, client, clientAuthentication, parameters, options) {
|
|
600
583
|
assertAs(as);
|
|
601
584
|
assertClient(client);
|
|
602
|
-
const url = resolveEndpoint(as, 'pushed_authorization_request_endpoint',
|
|
585
|
+
const url = resolveEndpoint(as, 'pushed_authorization_request_endpoint', client.use_mtls_endpoint_aliases, options?.[allowInsecureRequests] !== true);
|
|
603
586
|
const body = new URLSearchParams(parameters);
|
|
604
587
|
body.set('client_id', client.client_id);
|
|
605
588
|
const headers = prepareHeaders(options?.headers);
|
|
606
589
|
headers.set('accept', 'application/json');
|
|
607
590
|
if (options?.DPoP !== undefined) {
|
|
608
|
-
|
|
591
|
+
assertDPoP(options.DPoP);
|
|
592
|
+
await options.DPoP.addProof(url, headers, 'POST');
|
|
609
593
|
}
|
|
610
|
-
|
|
594
|
+
const response = await authenticatedRequest(as, client, clientAuthentication, url, body, headers, options);
|
|
595
|
+
options?.DPoP?.cacheNonce(response);
|
|
596
|
+
return response;
|
|
611
597
|
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
598
|
+
class DPoPHandler {
|
|
599
|
+
#header;
|
|
600
|
+
#privateKey;
|
|
601
|
+
#publicKey;
|
|
602
|
+
#clockSkew;
|
|
603
|
+
#modifyAssertion;
|
|
604
|
+
#map;
|
|
605
|
+
constructor(client, keyPair, options) {
|
|
606
|
+
assertPrivateKey(keyPair?.privateKey, '"DPoP.privateKey"');
|
|
607
|
+
assertPublicKey(keyPair?.publicKey, '"DPoP.publicKey"');
|
|
608
|
+
if (!keyPair.publicKey.extractable) {
|
|
609
|
+
throw CodedTypeError('"DPoP.publicKey.extractable" must be true', ERR_INVALID_ARG_VALUE);
|
|
610
|
+
}
|
|
611
|
+
this.#modifyAssertion = options?.[modifyAssertion];
|
|
612
|
+
this.#clockSkew = getClockSkew(client);
|
|
613
|
+
this.#privateKey = keyPair.privateKey;
|
|
614
|
+
this.#publicKey = keyPair.publicKey;
|
|
615
|
+
branded.add(this);
|
|
616
|
+
}
|
|
617
|
+
#get(key) {
|
|
618
|
+
this.#map ||= new Map();
|
|
619
|
+
let item = this.#map.get(key);
|
|
620
|
+
if (item) {
|
|
621
|
+
this.#map.delete(key);
|
|
622
|
+
this.#map.set(key, item);
|
|
623
|
+
}
|
|
624
|
+
return item;
|
|
625
|
+
}
|
|
626
|
+
#set(key, val) {
|
|
627
|
+
this.#map ||= new Map();
|
|
628
|
+
this.#map.delete(key);
|
|
629
|
+
if (this.#map.size === 100) {
|
|
630
|
+
this.#map.delete(this.#map.keys().next().value);
|
|
631
|
+
}
|
|
632
|
+
this.#map.set(key, val);
|
|
633
|
+
}
|
|
634
|
+
async addProof(url, headers, htm, accessToken) {
|
|
635
|
+
this.#header ||= {
|
|
636
|
+
alg: keyToJws(this.#privateKey),
|
|
637
|
+
typ: 'dpop+jwt',
|
|
638
|
+
jwk: await publicJwk(this.#publicKey),
|
|
639
|
+
};
|
|
640
|
+
const nonce = this.#get(url.origin);
|
|
641
|
+
const now = epochTime() + this.#clockSkew;
|
|
642
|
+
const payload = {
|
|
643
|
+
iat: now,
|
|
644
|
+
jti: randomBytes(),
|
|
645
|
+
htm,
|
|
646
|
+
nonce,
|
|
647
|
+
htu: `${url.origin}${url.pathname}`,
|
|
648
|
+
ath: accessToken ? b64u(await crypto.subtle.digest('SHA-256', buf(accessToken))) : undefined,
|
|
649
|
+
};
|
|
650
|
+
this.#modifyAssertion?.(this.#header, payload);
|
|
651
|
+
headers.set('dpop', await signJwt(this.#header, payload, this.#privateKey));
|
|
652
|
+
}
|
|
653
|
+
cacheNonce(response) {
|
|
654
|
+
try {
|
|
655
|
+
const nonce = response.headers.get('dpop-nonce');
|
|
656
|
+
if (nonce) {
|
|
657
|
+
this.#set(new URL(response.url).origin, nonce);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
catch { }
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
export function isDPoPNonceError(err) {
|
|
664
|
+
if (err instanceof WWWAuthenticateChallengeError) {
|
|
665
|
+
const { 0: challenge, length } = err.cause;
|
|
666
|
+
return (length === 1 && challenge.scheme === 'dpop' && challenge.parameters.error === 'use_dpop_nonce');
|
|
667
|
+
}
|
|
668
|
+
if (err instanceof ResponseBodyError) {
|
|
669
|
+
return err.error === 'use_dpop_nonce';
|
|
670
|
+
}
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
673
|
+
export function DPoP(client, keyPair, options) {
|
|
674
|
+
return new DPoPHandler(client, keyPair, options);
|
|
675
|
+
}
|
|
676
|
+
export class ResponseBodyError extends Error {
|
|
677
|
+
cause;
|
|
678
|
+
code;
|
|
679
|
+
error;
|
|
680
|
+
status;
|
|
681
|
+
error_description;
|
|
682
|
+
response;
|
|
683
|
+
constructor(message, options) {
|
|
684
|
+
super(message, options);
|
|
685
|
+
this.name = this.constructor.name;
|
|
686
|
+
this.code = RESPONSE_BODY_ERROR;
|
|
687
|
+
this.cause = options.cause;
|
|
688
|
+
this.error = options.cause.error;
|
|
689
|
+
this.status = options.response.status;
|
|
690
|
+
this.error_description = options.cause.error_description;
|
|
691
|
+
Object.defineProperty(this, 'response', { enumerable: false, value: options.response });
|
|
692
|
+
Error.captureStackTrace?.(this, this.constructor);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
export class AuthorizationResponseError extends Error {
|
|
696
|
+
cause;
|
|
697
|
+
code;
|
|
698
|
+
error;
|
|
699
|
+
error_description;
|
|
700
|
+
constructor(message, options) {
|
|
701
|
+
super(message, options);
|
|
702
|
+
this.name = this.constructor.name;
|
|
703
|
+
this.code = AUTHORIZATION_RESPONSE_ERROR;
|
|
704
|
+
this.cause = options.cause;
|
|
705
|
+
this.error = options.cause.get('error');
|
|
706
|
+
this.error_description = options.cause.get('error_description') ?? undefined;
|
|
707
|
+
Error.captureStackTrace?.(this, this.constructor);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
export class WWWAuthenticateChallengeError extends Error {
|
|
711
|
+
cause;
|
|
712
|
+
code;
|
|
713
|
+
response;
|
|
714
|
+
status;
|
|
715
|
+
constructor(message, options) {
|
|
716
|
+
super(message, options);
|
|
717
|
+
this.name = this.constructor.name;
|
|
718
|
+
this.code = WWW_AUTHENTICATE_CHALLENGE;
|
|
719
|
+
this.cause = options.cause;
|
|
720
|
+
this.status = options.response.status;
|
|
721
|
+
this.response = options.response;
|
|
722
|
+
Object.defineProperty(this, 'response', { enumerable: false });
|
|
723
|
+
Error.captureStackTrace?.(this, this.constructor);
|
|
616
724
|
}
|
|
617
|
-
return value.error !== undefined;
|
|
618
725
|
}
|
|
619
726
|
function unquote(value) {
|
|
620
727
|
if (value.length >= 2 && value[0] === '"' && value[value.length - 1] === '"') {
|
|
@@ -646,9 +753,9 @@ function wwwAuth(scheme, params) {
|
|
|
646
753
|
parameters,
|
|
647
754
|
};
|
|
648
755
|
}
|
|
649
|
-
|
|
756
|
+
function parseWwwAuthenticateChallenges(response) {
|
|
650
757
|
if (!looseInstanceOf(response, Response)) {
|
|
651
|
-
throw
|
|
758
|
+
throw CodedTypeError('"response" must be an instance of Response', ERR_INVALID_ARG_TYPE);
|
|
652
759
|
}
|
|
653
760
|
const header = response.headers.get('www-authenticate');
|
|
654
761
|
if (header === null) {
|
|
@@ -678,61 +785,88 @@ export async function processPushedAuthorizationResponse(as, client, response) {
|
|
|
678
785
|
assertAs(as);
|
|
679
786
|
assertClient(client);
|
|
680
787
|
if (!looseInstanceOf(response, Response)) {
|
|
681
|
-
throw
|
|
788
|
+
throw CodedTypeError('"response" must be an instance of Response', ERR_INVALID_ARG_TYPE);
|
|
789
|
+
}
|
|
790
|
+
let challenges;
|
|
791
|
+
if ((challenges = parseWwwAuthenticateChallenges(response))) {
|
|
792
|
+
throw new WWWAuthenticateChallengeError('server responded with a challenge in the WWW-Authenticate HTTP Header', { cause: challenges, response });
|
|
682
793
|
}
|
|
683
794
|
if (response.status !== 201) {
|
|
684
795
|
let err;
|
|
685
796
|
if ((err = await handleOAuthBodyError(response))) {
|
|
686
|
-
|
|
797
|
+
await response.body?.cancel();
|
|
798
|
+
throw new ResponseBodyError('server responded with an error in the response body', {
|
|
799
|
+
cause: err,
|
|
800
|
+
response,
|
|
801
|
+
});
|
|
687
802
|
}
|
|
688
|
-
throw
|
|
803
|
+
throw OPE('"response" is not a conform Pushed Authorization Request Endpoint response', RESPONSE_IS_NOT_CONFORM, response);
|
|
689
804
|
}
|
|
690
805
|
assertReadableResponse(response);
|
|
806
|
+
assertApplicationJson(response);
|
|
691
807
|
let json;
|
|
692
808
|
try {
|
|
693
809
|
json = await response.json();
|
|
694
810
|
}
|
|
695
811
|
catch (cause) {
|
|
696
|
-
throw
|
|
812
|
+
throw OPE('failed to parse "response" body as JSON', PARSE_ERROR, cause);
|
|
697
813
|
}
|
|
698
814
|
if (!isJsonObject(json)) {
|
|
699
|
-
throw
|
|
700
|
-
}
|
|
701
|
-
if (!validateString(json.request_uri)) {
|
|
702
|
-
throw new OPE('"response" body "request_uri" property must be a non-empty string');
|
|
703
|
-
}
|
|
704
|
-
if (typeof json.expires_in !== 'number' || json.expires_in <= 0) {
|
|
705
|
-
throw new OPE('"response" body "expires_in" property must be a positive number');
|
|
815
|
+
throw OPE('"response" body must be a top level object', INVALID_RESPONSE, { body: json });
|
|
706
816
|
}
|
|
817
|
+
assertString(json.request_uri, '"response" body "request_uri" property', INVALID_RESPONSE, {
|
|
818
|
+
body: json,
|
|
819
|
+
});
|
|
820
|
+
let expiresIn = typeof json.expires_in !== 'number' ? parseFloat(json.expires_in) : json.expires_in;
|
|
821
|
+
assertNumber(expiresIn, false, '"response" body "expires_in" property', INVALID_RESPONSE, {
|
|
822
|
+
body: json,
|
|
823
|
+
});
|
|
824
|
+
json.expires_in = expiresIn;
|
|
707
825
|
return json;
|
|
708
826
|
}
|
|
709
|
-
|
|
710
|
-
if (!
|
|
711
|
-
throw
|
|
827
|
+
function assertDPoP(option) {
|
|
828
|
+
if (!branded.has(option)) {
|
|
829
|
+
throw CodedTypeError('"options.DPoP" is not a valid DPoPHandle', ERR_INVALID_ARG_VALUE);
|
|
712
830
|
}
|
|
831
|
+
}
|
|
832
|
+
async function resourceRequest(accessToken, method, url, headers, body, options) {
|
|
833
|
+
assertString(accessToken, '"accessToken"');
|
|
713
834
|
if (!(url instanceof URL)) {
|
|
714
|
-
throw
|
|
835
|
+
throw CodedTypeError('"url" must be an instance of URL', ERR_INVALID_ARG_TYPE);
|
|
715
836
|
}
|
|
837
|
+
checkProtocol(url, options?.[allowInsecureRequests] !== true);
|
|
716
838
|
headers = prepareHeaders(headers);
|
|
717
|
-
if (options?.DPoP
|
|
718
|
-
|
|
839
|
+
if (options?.DPoP) {
|
|
840
|
+
assertDPoP(options.DPoP);
|
|
841
|
+
await options.DPoP.addProof(url, headers, method.toUpperCase(), accessToken);
|
|
842
|
+
headers.set('authorization', `DPoP ${accessToken}`);
|
|
719
843
|
}
|
|
720
844
|
else {
|
|
721
|
-
|
|
722
|
-
headers.set('authorization', `DPoP ${accessToken}`);
|
|
845
|
+
headers.set('authorization', `Bearer ${accessToken}`);
|
|
723
846
|
}
|
|
724
|
-
|
|
847
|
+
const response = await (options?.[customFetch] || fetch)(url.href, {
|
|
725
848
|
body,
|
|
726
849
|
headers: Object.fromEntries(headers.entries()),
|
|
727
850
|
method,
|
|
728
851
|
redirect: 'manual',
|
|
729
|
-
signal: options?.signal ? signal(options.signal) :
|
|
730
|
-
})
|
|
852
|
+
signal: options?.signal ? signal(options.signal) : undefined,
|
|
853
|
+
});
|
|
854
|
+
options?.DPoP?.cacheNonce(response);
|
|
855
|
+
return response;
|
|
856
|
+
}
|
|
857
|
+
export async function protectedResourceRequest(accessToken, method, url, headers, body, options) {
|
|
858
|
+
return resourceRequest(accessToken, method, url, headers, body, options).then((response) => {
|
|
859
|
+
let challenges;
|
|
860
|
+
if ((challenges = parseWwwAuthenticateChallenges(response))) {
|
|
861
|
+
throw new WWWAuthenticateChallengeError('server responded with a challenge in the WWW-Authenticate HTTP Header', { cause: challenges, response });
|
|
862
|
+
}
|
|
863
|
+
return response;
|
|
864
|
+
});
|
|
731
865
|
}
|
|
732
866
|
export async function userInfoRequest(as, client, accessToken, options) {
|
|
733
867
|
assertAs(as);
|
|
734
868
|
assertClient(client);
|
|
735
|
-
const url = resolveEndpoint(as, 'userinfo_endpoint',
|
|
869
|
+
const url = resolveEndpoint(as, 'userinfo_endpoint', client.use_mtls_endpoint_aliases, options?.[allowInsecureRequests] !== true);
|
|
736
870
|
const headers = prepareHeaders(options?.headers);
|
|
737
871
|
if (client.userinfo_signed_response_alg) {
|
|
738
872
|
headers.set('accept', 'application/jwt');
|
|
@@ -741,14 +875,14 @@ export async function userInfoRequest(as, client, accessToken, options) {
|
|
|
741
875
|
headers.set('accept', 'application/json');
|
|
742
876
|
headers.append('accept', 'application/jwt');
|
|
743
877
|
}
|
|
744
|
-
return
|
|
878
|
+
return resourceRequest(accessToken, 'GET', url, headers, null, {
|
|
745
879
|
...options,
|
|
746
880
|
[clockSkew]: getClockSkew(client),
|
|
747
881
|
});
|
|
748
882
|
}
|
|
749
883
|
let jwksMap;
|
|
750
884
|
function setJwksCache(as, jwks, uat, cache) {
|
|
751
|
-
jwksMap
|
|
885
|
+
jwksMap ||= new WeakMap();
|
|
752
886
|
jwksMap.set(as, {
|
|
753
887
|
jwks,
|
|
754
888
|
uat,
|
|
@@ -782,7 +916,7 @@ function clearJwksCache(as, cache) {
|
|
|
782
916
|
}
|
|
783
917
|
async function getPublicSigKeyFromIssuerJwksUri(as, options, header) {
|
|
784
918
|
const { alg, kid } = header;
|
|
785
|
-
checkSupportedJwsAlg(
|
|
919
|
+
checkSupportedJwsAlg(header);
|
|
786
920
|
if (!jwksMap?.has(as) && isFreshJwksCache(options?.[jwksCache])) {
|
|
787
921
|
setJwksCache(as, options?.[jwksCache].jwks, options?.[jwksCache].uat);
|
|
788
922
|
}
|
|
@@ -814,7 +948,7 @@ async function getPublicSigKeyFromIssuerJwksUri(as, options, header) {
|
|
|
814
948
|
kty = 'OKP';
|
|
815
949
|
break;
|
|
816
950
|
default:
|
|
817
|
-
throw new UnsupportedOperationError();
|
|
951
|
+
throw new UnsupportedOperationError('unsupported JWS algorithm', { cause: { alg } });
|
|
818
952
|
}
|
|
819
953
|
const candidates = jwks.keys.filter((jwk) => {
|
|
820
954
|
if (jwk.kty !== kty) {
|
|
@@ -836,7 +970,8 @@ async function getPublicSigKeyFromIssuerJwksUri(as, options, header) {
|
|
|
836
970
|
case alg === 'ES256' && jwk.crv !== 'P-256':
|
|
837
971
|
case alg === 'ES384' && jwk.crv !== 'P-384':
|
|
838
972
|
case alg === 'ES512' && jwk.crv !== 'P-521':
|
|
839
|
-
case alg === '
|
|
973
|
+
case alg === 'Ed25519' && jwk.crv !== 'Ed25519':
|
|
974
|
+
case alg === 'EdDSA' && jwk.crv !== 'Ed25519':
|
|
840
975
|
return false;
|
|
841
976
|
}
|
|
842
977
|
return true;
|
|
@@ -847,221 +982,223 @@ async function getPublicSigKeyFromIssuerJwksUri(as, options, header) {
|
|
|
847
982
|
clearJwksCache(as, options?.[jwksCache]);
|
|
848
983
|
return getPublicSigKeyFromIssuerJwksUri(as, options, header);
|
|
849
984
|
}
|
|
850
|
-
throw
|
|
985
|
+
throw OPE('error when selecting a JWT verification key, no applicable keys found', KEY_SELECTION, { header, candidates, jwks_uri: new URL(as.jwks_uri) });
|
|
851
986
|
}
|
|
852
987
|
if (length !== 1) {
|
|
853
|
-
throw
|
|
854
|
-
}
|
|
855
|
-
const key = await importJwk(alg, jwk);
|
|
856
|
-
if (key.type !== 'public') {
|
|
857
|
-
throw new OPE('jwks_uri must only contain public keys');
|
|
988
|
+
throw OPE('error when selecting a JWT verification key, multiple applicable keys found, a "kid" JWT Header Parameter is required', KEY_SELECTION, { header, candidates, jwks_uri: new URL(as.jwks_uri) });
|
|
858
989
|
}
|
|
859
|
-
return
|
|
990
|
+
return importJwk(alg, jwk);
|
|
860
991
|
}
|
|
861
992
|
export const skipSubjectCheck = Symbol();
|
|
862
993
|
function getContentType(response) {
|
|
863
994
|
return response.headers.get('content-type')?.split(';')[0];
|
|
864
995
|
}
|
|
865
|
-
export async function processUserInfoResponse(as, client, expectedSubject, response) {
|
|
996
|
+
export async function processUserInfoResponse(as, client, expectedSubject, response, options) {
|
|
866
997
|
assertAs(as);
|
|
867
998
|
assertClient(client);
|
|
868
999
|
if (!looseInstanceOf(response, Response)) {
|
|
869
|
-
throw
|
|
1000
|
+
throw CodedTypeError('"response" must be an instance of Response', ERR_INVALID_ARG_TYPE);
|
|
1001
|
+
}
|
|
1002
|
+
let challenges;
|
|
1003
|
+
if ((challenges = parseWwwAuthenticateChallenges(response))) {
|
|
1004
|
+
throw new WWWAuthenticateChallengeError('server responded with a challenge in the WWW-Authenticate HTTP Header', { cause: challenges, response });
|
|
870
1005
|
}
|
|
871
1006
|
if (response.status !== 200) {
|
|
872
|
-
throw
|
|
1007
|
+
throw OPE('"response" is not a conform UserInfo Endpoint response', RESPONSE_IS_NOT_CONFORM, response);
|
|
873
1008
|
}
|
|
1009
|
+
assertReadableResponse(response);
|
|
874
1010
|
let json;
|
|
875
1011
|
if (getContentType(response) === 'application/jwt') {
|
|
876
|
-
|
|
877
|
-
const { claims, jwt } = await validateJwt(await response.text(), checkSigningAlgorithm.bind(undefined, client.userinfo_signed_response_alg, as.userinfo_signing_alg_values_supported), noSignatureCheck, getClockSkew(client), getClockTolerance(client), client[jweDecrypt])
|
|
1012
|
+
const { claims, jwt } = await validateJwt(await response.text(), checkSigningAlgorithm.bind(undefined, client.userinfo_signed_response_alg, as.userinfo_signing_alg_values_supported, undefined), getClockSkew(client), getClockTolerance(client), options?.[jweDecrypt])
|
|
878
1013
|
.then(validateOptionalAudience.bind(undefined, client.client_id))
|
|
879
|
-
.then(validateOptionalIssuer.bind(undefined, as
|
|
880
|
-
|
|
1014
|
+
.then(validateOptionalIssuer.bind(undefined, as));
|
|
1015
|
+
jwtRefs.set(response, jwt);
|
|
881
1016
|
json = claims;
|
|
882
1017
|
}
|
|
883
1018
|
else {
|
|
884
1019
|
if (client.userinfo_signed_response_alg) {
|
|
885
|
-
throw
|
|
1020
|
+
throw OPE('JWT UserInfo Response expected', JWT_USERINFO_EXPECTED, response);
|
|
886
1021
|
}
|
|
887
|
-
|
|
1022
|
+
assertApplicationJson(response);
|
|
888
1023
|
try {
|
|
889
1024
|
json = await response.json();
|
|
890
1025
|
}
|
|
891
1026
|
catch (cause) {
|
|
892
|
-
throw
|
|
1027
|
+
throw OPE('failed to parse "response" body as JSON', PARSE_ERROR, cause);
|
|
893
1028
|
}
|
|
894
1029
|
}
|
|
895
1030
|
if (!isJsonObject(json)) {
|
|
896
|
-
throw
|
|
897
|
-
}
|
|
898
|
-
if (!validateString(json.sub)) {
|
|
899
|
-
throw new OPE('"response" body "sub" property must be a non-empty string');
|
|
1031
|
+
throw OPE('"response" body must be a top level object', INVALID_RESPONSE, { body: json });
|
|
900
1032
|
}
|
|
1033
|
+
assertString(json.sub, '"response" body "sub" property', INVALID_RESPONSE, { body: json });
|
|
901
1034
|
switch (expectedSubject) {
|
|
902
1035
|
case skipSubjectCheck:
|
|
903
1036
|
break;
|
|
904
1037
|
default:
|
|
905
|
-
|
|
906
|
-
throw new OPE('"expectedSubject" must be a non-empty string');
|
|
907
|
-
}
|
|
1038
|
+
assertString(expectedSubject, '"expectedSubject"');
|
|
908
1039
|
if (json.sub !== expectedSubject) {
|
|
909
|
-
throw
|
|
1040
|
+
throw OPE('unexpected "response" body "sub" property value', JSON_ATTRIBUTE_COMPARISON, {
|
|
1041
|
+
expected: expectedSubject,
|
|
1042
|
+
body: json,
|
|
1043
|
+
attribute: 'sub',
|
|
1044
|
+
});
|
|
910
1045
|
}
|
|
911
1046
|
}
|
|
912
1047
|
return json;
|
|
913
1048
|
}
|
|
914
|
-
async function authenticatedRequest(as, client,
|
|
915
|
-
await clientAuthentication(as, client, body, headers
|
|
1049
|
+
async function authenticatedRequest(as, client, clientAuthentication, url, body, headers, options) {
|
|
1050
|
+
await clientAuthentication(as, client, body, headers);
|
|
916
1051
|
headers.set('content-type', 'application/x-www-form-urlencoded;charset=UTF-8');
|
|
917
1052
|
return (options?.[customFetch] || fetch)(url.href, {
|
|
918
1053
|
body,
|
|
919
1054
|
headers: Object.fromEntries(headers.entries()),
|
|
920
|
-
method,
|
|
1055
|
+
method: 'POST',
|
|
921
1056
|
redirect: 'manual',
|
|
922
|
-
signal: options?.signal ? signal(options.signal) :
|
|
923
|
-
})
|
|
1057
|
+
signal: options?.signal ? signal(options.signal) : undefined,
|
|
1058
|
+
});
|
|
924
1059
|
}
|
|
925
|
-
async function tokenEndpointRequest(as, client, grantType, parameters, options) {
|
|
926
|
-
const url = resolveEndpoint(as, 'token_endpoint',
|
|
1060
|
+
async function tokenEndpointRequest(as, client, clientAuthentication, grantType, parameters, options) {
|
|
1061
|
+
const url = resolveEndpoint(as, 'token_endpoint', client.use_mtls_endpoint_aliases, options?.[allowInsecureRequests] !== true);
|
|
927
1062
|
parameters.set('grant_type', grantType);
|
|
928
1063
|
const headers = prepareHeaders(options?.headers);
|
|
929
1064
|
headers.set('accept', 'application/json');
|
|
930
1065
|
if (options?.DPoP !== undefined) {
|
|
931
|
-
|
|
1066
|
+
assertDPoP(options.DPoP);
|
|
1067
|
+
await options.DPoP.addProof(url, headers, 'POST');
|
|
932
1068
|
}
|
|
933
|
-
|
|
1069
|
+
const response = await authenticatedRequest(as, client, clientAuthentication, url, parameters, headers, options);
|
|
1070
|
+
options?.DPoP?.cacheNonce(response);
|
|
1071
|
+
return response;
|
|
934
1072
|
}
|
|
935
|
-
export async function refreshTokenGrantRequest(as, client, refreshToken, options) {
|
|
1073
|
+
export async function refreshTokenGrantRequest(as, client, clientAuthentication, refreshToken, options) {
|
|
936
1074
|
assertAs(as);
|
|
937
1075
|
assertClient(client);
|
|
938
|
-
|
|
939
|
-
throw new TypeError('"refreshToken" must be a non-empty string');
|
|
940
|
-
}
|
|
1076
|
+
assertString(refreshToken, '"refreshToken"');
|
|
941
1077
|
const parameters = new URLSearchParams(options?.additionalParameters);
|
|
942
1078
|
parameters.set('refresh_token', refreshToken);
|
|
943
|
-
return tokenEndpointRequest(as, client, 'refresh_token', parameters, options);
|
|
1079
|
+
return tokenEndpointRequest(as, client, clientAuthentication, 'refresh_token', parameters, options);
|
|
944
1080
|
}
|
|
945
1081
|
const idTokenClaims = new WeakMap();
|
|
946
|
-
const
|
|
1082
|
+
const jwtRefs = new WeakMap();
|
|
947
1083
|
export function getValidatedIdTokenClaims(ref) {
|
|
948
1084
|
if (!ref.id_token) {
|
|
949
1085
|
return undefined;
|
|
950
1086
|
}
|
|
951
1087
|
const claims = idTokenClaims.get(ref);
|
|
952
1088
|
if (!claims) {
|
|
953
|
-
throw
|
|
954
|
-
}
|
|
955
|
-
return claims[0];
|
|
956
|
-
}
|
|
957
|
-
export async function validateIdTokenSignature(as, ref, options) {
|
|
958
|
-
assertAs(as);
|
|
959
|
-
if (!idTokenClaims.has(ref)) {
|
|
960
|
-
throw new OPE('"ref" does not contain an ID Token to verify the signature of');
|
|
1089
|
+
throw CodedTypeError('"ref" was already garbage collected or did not resolve from the proper sources', ERR_INVALID_ARG_VALUE);
|
|
961
1090
|
}
|
|
962
|
-
|
|
963
|
-
const header = JSON.parse(buf(b64u(protectedHeader)));
|
|
964
|
-
if (header.alg.startsWith('HS')) {
|
|
965
|
-
throw new UnsupportedOperationError();
|
|
966
|
-
}
|
|
967
|
-
let key;
|
|
968
|
-
key = await getPublicSigKeyFromIssuerJwksUri(as, options, header);
|
|
969
|
-
await validateJwsSignature(protectedHeader, payload, key, b64u(encodedSignature));
|
|
1091
|
+
return claims;
|
|
970
1092
|
}
|
|
971
|
-
async function
|
|
1093
|
+
export async function validateApplicationLevelSignature(as, ref, options) {
|
|
972
1094
|
assertAs(as);
|
|
973
|
-
if (!
|
|
974
|
-
throw
|
|
1095
|
+
if (!jwtRefs.has(ref)) {
|
|
1096
|
+
throw CodedTypeError('"ref" does not contain a processed JWT Response to verify the signature of', ERR_INVALID_ARG_VALUE);
|
|
975
1097
|
}
|
|
976
|
-
const { 0: protectedHeader, 1: payload, 2: encodedSignature
|
|
1098
|
+
const { 0: protectedHeader, 1: payload, 2: encodedSignature } = jwtRefs.get(ref).split('.');
|
|
977
1099
|
const header = JSON.parse(buf(b64u(protectedHeader)));
|
|
978
1100
|
if (header.alg.startsWith('HS')) {
|
|
979
|
-
throw new UnsupportedOperationError();
|
|
1101
|
+
throw new UnsupportedOperationError('unsupported JWS algorithm', { cause: { alg: header.alg } });
|
|
980
1102
|
}
|
|
981
1103
|
let key;
|
|
982
1104
|
key = await getPublicSigKeyFromIssuerJwksUri(as, options, header);
|
|
983
1105
|
await validateJwsSignature(protectedHeader, payload, key, b64u(encodedSignature));
|
|
984
1106
|
}
|
|
985
|
-
|
|
986
|
-
return validateJwtResponseSignature(as, ref, options);
|
|
987
|
-
}
|
|
988
|
-
export function validateJwtIntrospectionSignature(as, ref, options) {
|
|
989
|
-
return validateJwtResponseSignature(as, ref, options);
|
|
990
|
-
}
|
|
991
|
-
async function processGenericAccessTokenResponse(as, client, response, ignoreIdToken = false, ignoreRefreshToken = false) {
|
|
1107
|
+
async function processGenericAccessTokenResponse(as, client, response, additionalRequiredIdTokenClaims, options) {
|
|
992
1108
|
assertAs(as);
|
|
993
1109
|
assertClient(client);
|
|
994
1110
|
if (!looseInstanceOf(response, Response)) {
|
|
995
|
-
throw
|
|
1111
|
+
throw CodedTypeError('"response" must be an instance of Response', ERR_INVALID_ARG_TYPE);
|
|
1112
|
+
}
|
|
1113
|
+
let challenges;
|
|
1114
|
+
if ((challenges = parseWwwAuthenticateChallenges(response))) {
|
|
1115
|
+
throw new WWWAuthenticateChallengeError('server responded with a challenge in the WWW-Authenticate HTTP Header', { cause: challenges, response });
|
|
996
1116
|
}
|
|
997
1117
|
if (response.status !== 200) {
|
|
998
1118
|
let err;
|
|
999
1119
|
if ((err = await handleOAuthBodyError(response))) {
|
|
1000
|
-
|
|
1120
|
+
await response.body?.cancel();
|
|
1121
|
+
throw new ResponseBodyError('server responded with an error in the response body', {
|
|
1122
|
+
cause: err,
|
|
1123
|
+
response,
|
|
1124
|
+
});
|
|
1001
1125
|
}
|
|
1002
|
-
throw
|
|
1126
|
+
throw OPE('"response" is not a conform Token Endpoint response', RESPONSE_IS_NOT_CONFORM, response);
|
|
1003
1127
|
}
|
|
1004
1128
|
assertReadableResponse(response);
|
|
1129
|
+
assertApplicationJson(response);
|
|
1005
1130
|
let json;
|
|
1006
1131
|
try {
|
|
1007
1132
|
json = await response.json();
|
|
1008
1133
|
}
|
|
1009
1134
|
catch (cause) {
|
|
1010
|
-
throw
|
|
1135
|
+
throw OPE('failed to parse "response" body as JSON', PARSE_ERROR, cause);
|
|
1011
1136
|
}
|
|
1012
1137
|
if (!isJsonObject(json)) {
|
|
1013
|
-
throw
|
|
1014
|
-
}
|
|
1015
|
-
if (!validateString(json.access_token)) {
|
|
1016
|
-
throw new OPE('"response" body "access_token" property must be a non-empty string');
|
|
1017
|
-
}
|
|
1018
|
-
if (!validateString(json.token_type)) {
|
|
1019
|
-
throw new OPE('"response" body "token_type" property must be a non-empty string');
|
|
1138
|
+
throw OPE('"response" body must be a top level object', INVALID_RESPONSE, { body: json });
|
|
1020
1139
|
}
|
|
1140
|
+
assertString(json.access_token, '"response" body "access_token" property', INVALID_RESPONSE, {
|
|
1141
|
+
body: json,
|
|
1142
|
+
});
|
|
1143
|
+
assertString(json.token_type, '"response" body "token_type" property', INVALID_RESPONSE, {
|
|
1144
|
+
body: json,
|
|
1145
|
+
});
|
|
1021
1146
|
json.token_type = json.token_type.toLowerCase();
|
|
1022
1147
|
if (json.token_type !== 'dpop' && json.token_type !== 'bearer') {
|
|
1023
|
-
throw new UnsupportedOperationError('unsupported `token_type` value');
|
|
1148
|
+
throw new UnsupportedOperationError('unsupported `token_type` value', { cause: { body: json } });
|
|
1024
1149
|
}
|
|
1025
|
-
if (json.expires_in !== undefined
|
|
1026
|
-
|
|
1027
|
-
|
|
1150
|
+
if (json.expires_in !== undefined) {
|
|
1151
|
+
let expiresIn = typeof json.expires_in !== 'number' ? parseFloat(json.expires_in) : json.expires_in;
|
|
1152
|
+
assertNumber(expiresIn, false, '"response" body "expires_in" property', INVALID_RESPONSE, {
|
|
1153
|
+
body: json,
|
|
1154
|
+
});
|
|
1155
|
+
json.expires_in = expiresIn;
|
|
1028
1156
|
}
|
|
1029
|
-
if (
|
|
1030
|
-
json.refresh_token
|
|
1031
|
-
|
|
1032
|
-
|
|
1157
|
+
if (json.refresh_token !== undefined) {
|
|
1158
|
+
assertString(json.refresh_token, '"response" body "refresh_token" property', INVALID_RESPONSE, {
|
|
1159
|
+
body: json,
|
|
1160
|
+
});
|
|
1033
1161
|
}
|
|
1034
1162
|
if (json.scope !== undefined && typeof json.scope !== 'string') {
|
|
1035
|
-
throw
|
|
1036
|
-
}
|
|
1037
|
-
if (
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1163
|
+
throw OPE('"response" body "scope" property must be a string', INVALID_RESPONSE, { body: json });
|
|
1164
|
+
}
|
|
1165
|
+
if (json.id_token !== undefined) {
|
|
1166
|
+
assertString(json.id_token, '"response" body "id_token" property', INVALID_RESPONSE, {
|
|
1167
|
+
body: json,
|
|
1168
|
+
});
|
|
1169
|
+
const requiredClaims = ['aud', 'exp', 'iat', 'iss', 'sub'];
|
|
1170
|
+
if (client.require_auth_time === true) {
|
|
1171
|
+
requiredClaims.push('auth_time');
|
|
1172
|
+
}
|
|
1173
|
+
if (client.default_max_age !== undefined) {
|
|
1174
|
+
assertNumber(client.default_max_age, false, '"client.default_max_age"');
|
|
1175
|
+
requiredClaims.push('auth_time');
|
|
1176
|
+
}
|
|
1177
|
+
if (additionalRequiredIdTokenClaims?.length) {
|
|
1178
|
+
requiredClaims.push(...additionalRequiredIdTokenClaims);
|
|
1179
|
+
}
|
|
1180
|
+
const { claims, jwt } = await validateJwt(json.id_token, checkSigningAlgorithm.bind(undefined, client.id_token_signed_response_alg, as.id_token_signing_alg_values_supported, 'RS256'), getClockSkew(client), getClockTolerance(client), options?.[jweDecrypt])
|
|
1181
|
+
.then(validatePresence.bind(undefined, requiredClaims))
|
|
1182
|
+
.then(validateIssuer.bind(undefined, as))
|
|
1183
|
+
.then(validateAudience.bind(undefined, client.client_id));
|
|
1184
|
+
if (Array.isArray(claims.aud) && claims.aud.length !== 1) {
|
|
1185
|
+
if (claims.azp === undefined) {
|
|
1186
|
+
throw OPE('ID Token "aud" (audience) claim includes additional untrusted audiences', JWT_CLAIM_COMPARISON, { claims, claim: 'aud' });
|
|
1053
1187
|
}
|
|
1054
|
-
if (claims.
|
|
1055
|
-
(
|
|
1056
|
-
throw new OPE('ID Token "auth_time" (authentication time) must be a positive number');
|
|
1188
|
+
if (claims.azp !== client.client_id) {
|
|
1189
|
+
throw OPE('unexpected ID Token "azp" (authorized party) claim value', JWT_CLAIM_COMPARISON, { expected: client.client_id, claims, claim: 'azp' });
|
|
1057
1190
|
}
|
|
1058
|
-
idTokenClaims.set(json, [claims, jwt]);
|
|
1059
1191
|
}
|
|
1192
|
+
if (claims.auth_time !== undefined) {
|
|
1193
|
+
assertNumber(claims.auth_time, false, 'ID Token "auth_time" (authentication time)', INVALID_RESPONSE, { claims });
|
|
1194
|
+
}
|
|
1195
|
+
jwtRefs.set(response, jwt);
|
|
1196
|
+
idTokenClaims.set(json, claims);
|
|
1060
1197
|
}
|
|
1061
1198
|
return json;
|
|
1062
1199
|
}
|
|
1063
|
-
export async function processRefreshTokenResponse(as, client, response) {
|
|
1064
|
-
return processGenericAccessTokenResponse(as, client, response);
|
|
1200
|
+
export async function processRefreshTokenResponse(as, client, response, options) {
|
|
1201
|
+
return processGenericAccessTokenResponse(as, client, response, undefined, options);
|
|
1065
1202
|
}
|
|
1066
1203
|
function validateOptionalAudience(expected, result) {
|
|
1067
1204
|
if (result.claims.aud !== undefined) {
|
|
@@ -1072,23 +1209,36 @@ function validateOptionalAudience(expected, result) {
|
|
|
1072
1209
|
function validateAudience(expected, result) {
|
|
1073
1210
|
if (Array.isArray(result.claims.aud)) {
|
|
1074
1211
|
if (!result.claims.aud.includes(expected)) {
|
|
1075
|
-
throw
|
|
1212
|
+
throw OPE('unexpected JWT "aud" (audience) claim value', JWT_CLAIM_COMPARISON, {
|
|
1213
|
+
expected,
|
|
1214
|
+
claims: result.claims,
|
|
1215
|
+
claim: 'aud',
|
|
1216
|
+
});
|
|
1076
1217
|
}
|
|
1077
1218
|
}
|
|
1078
1219
|
else if (result.claims.aud !== expected) {
|
|
1079
|
-
throw
|
|
1220
|
+
throw OPE('unexpected JWT "aud" (audience) claim value', JWT_CLAIM_COMPARISON, {
|
|
1221
|
+
expected,
|
|
1222
|
+
claims: result.claims,
|
|
1223
|
+
claim: 'aud',
|
|
1224
|
+
});
|
|
1080
1225
|
}
|
|
1081
1226
|
return result;
|
|
1082
1227
|
}
|
|
1083
|
-
function validateOptionalIssuer(
|
|
1228
|
+
function validateOptionalIssuer(as, result) {
|
|
1084
1229
|
if (result.claims.iss !== undefined) {
|
|
1085
|
-
return validateIssuer(
|
|
1230
|
+
return validateIssuer(as, result);
|
|
1086
1231
|
}
|
|
1087
1232
|
return result;
|
|
1088
1233
|
}
|
|
1089
|
-
function validateIssuer(
|
|
1234
|
+
function validateIssuer(as, result) {
|
|
1235
|
+
const expected = as[_expectedIssuer]?.(result) ?? as.issuer;
|
|
1090
1236
|
if (result.claims.iss !== expected) {
|
|
1091
|
-
throw
|
|
1237
|
+
throw OPE('unexpected JWT "iss" (issuer) claim value', JWT_CLAIM_COMPARISON, {
|
|
1238
|
+
expected,
|
|
1239
|
+
claims: result.claims,
|
|
1240
|
+
claim: 'iss',
|
|
1241
|
+
});
|
|
1092
1242
|
}
|
|
1093
1243
|
return result;
|
|
1094
1244
|
}
|
|
@@ -1097,27 +1247,25 @@ function brand(searchParams) {
|
|
|
1097
1247
|
branded.add(searchParams);
|
|
1098
1248
|
return searchParams;
|
|
1099
1249
|
}
|
|
1100
|
-
export async function authorizationCodeGrantRequest(as, client, callbackParameters, redirectUri, codeVerifier, options) {
|
|
1250
|
+
export async function authorizationCodeGrantRequest(as, client, clientAuthentication, callbackParameters, redirectUri, codeVerifier, options) {
|
|
1101
1251
|
assertAs(as);
|
|
1102
1252
|
assertClient(client);
|
|
1103
1253
|
if (!branded.has(callbackParameters)) {
|
|
1104
|
-
throw
|
|
1105
|
-
}
|
|
1106
|
-
if (!validateString(redirectUri)) {
|
|
1107
|
-
throw new TypeError('"redirectUri" must be a non-empty string');
|
|
1108
|
-
}
|
|
1109
|
-
if (!validateString(codeVerifier)) {
|
|
1110
|
-
throw new TypeError('"codeVerifier" must be a non-empty string');
|
|
1254
|
+
throw CodedTypeError('"callbackParameters" must be an instance of URLSearchParams obtained from "validateAuthResponse()", or "validateJwtAuthResponse()', ERR_INVALID_ARG_VALUE);
|
|
1111
1255
|
}
|
|
1256
|
+
assertString(redirectUri, '"redirectUri"');
|
|
1112
1257
|
const code = getURLSearchParameter(callbackParameters, 'code');
|
|
1113
1258
|
if (!code) {
|
|
1114
|
-
throw
|
|
1259
|
+
throw OPE('no authorization code in "callbackParameters"', INVALID_RESPONSE);
|
|
1115
1260
|
}
|
|
1116
1261
|
const parameters = new URLSearchParams(options?.additionalParameters);
|
|
1117
1262
|
parameters.set('redirect_uri', redirectUri);
|
|
1118
|
-
parameters.set('code_verifier', codeVerifier);
|
|
1119
1263
|
parameters.set('code', code);
|
|
1120
|
-
|
|
1264
|
+
if (codeVerifier !== _nopkce) {
|
|
1265
|
+
assertString(codeVerifier, '"codeVerifier"');
|
|
1266
|
+
parameters.set('code_verifier', codeVerifier);
|
|
1267
|
+
}
|
|
1268
|
+
return tokenEndpointRequest(as, client, clientAuthentication, 'authorization_code', parameters, options);
|
|
1121
1269
|
}
|
|
1122
1270
|
const jwtClaimNames = {
|
|
1123
1271
|
aud: 'audience',
|
|
@@ -1134,138 +1282,190 @@ const jwtClaimNames = {
|
|
|
1134
1282
|
htm: 'http method',
|
|
1135
1283
|
htu: 'http uri',
|
|
1136
1284
|
cnf: 'confirmation',
|
|
1285
|
+
auth_time: 'authentication time',
|
|
1137
1286
|
};
|
|
1138
1287
|
function validatePresence(required, result) {
|
|
1139
1288
|
for (const claim of required) {
|
|
1140
1289
|
if (result.claims[claim] === undefined) {
|
|
1141
|
-
throw
|
|
1290
|
+
throw OPE(`JWT "${claim}" (${jwtClaimNames[claim]}) claim missing`, INVALID_RESPONSE, {
|
|
1291
|
+
claims: result.claims,
|
|
1292
|
+
});
|
|
1142
1293
|
}
|
|
1143
1294
|
}
|
|
1144
1295
|
return result;
|
|
1145
1296
|
}
|
|
1146
1297
|
export const expectNoNonce = Symbol();
|
|
1147
1298
|
export const skipAuthTimeCheck = Symbol();
|
|
1148
|
-
export async function
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1299
|
+
export async function processAuthorizationCodeResponse(as, client, response, options) {
|
|
1300
|
+
if (typeof options?.expectedNonce === 'string' ||
|
|
1301
|
+
typeof options?.maxAge === 'number' ||
|
|
1302
|
+
options?.requireIdToken) {
|
|
1303
|
+
return processAuthorizationCodeOpenIDResponse(as, client, response, options.expectedNonce, options.maxAge, {
|
|
1304
|
+
[jweDecrypt]: options[jweDecrypt],
|
|
1305
|
+
});
|
|
1152
1306
|
}
|
|
1153
|
-
|
|
1154
|
-
|
|
1307
|
+
return processAuthorizationCodeOAuth2Response(as, client, response, options);
|
|
1308
|
+
}
|
|
1309
|
+
async function processAuthorizationCodeOpenIDResponse(as, client, response, expectedNonce, maxAge, options) {
|
|
1310
|
+
const additionalRequiredClaims = [];
|
|
1311
|
+
switch (expectedNonce) {
|
|
1312
|
+
case undefined:
|
|
1313
|
+
expectedNonce = expectNoNonce;
|
|
1314
|
+
break;
|
|
1315
|
+
case expectNoNonce:
|
|
1316
|
+
break;
|
|
1317
|
+
default:
|
|
1318
|
+
assertString(expectedNonce, '"expectedNonce" argument');
|
|
1319
|
+
additionalRequiredClaims.push('nonce');
|
|
1155
1320
|
}
|
|
1156
|
-
maxAge
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1321
|
+
maxAge ??= client.default_max_age;
|
|
1322
|
+
switch (maxAge) {
|
|
1323
|
+
case undefined:
|
|
1324
|
+
maxAge = skipAuthTimeCheck;
|
|
1325
|
+
break;
|
|
1326
|
+
case skipAuthTimeCheck:
|
|
1327
|
+
break;
|
|
1328
|
+
default:
|
|
1329
|
+
assertNumber(maxAge, false, '"maxAge" argument');
|
|
1330
|
+
additionalRequiredClaims.push('auth_time');
|
|
1161
1331
|
}
|
|
1332
|
+
const result = await processGenericAccessTokenResponse(as, client, response, additionalRequiredClaims, options);
|
|
1333
|
+
assertString(result.id_token, '"response" body "id_token" property', INVALID_RESPONSE, {
|
|
1334
|
+
body: result,
|
|
1335
|
+
});
|
|
1336
|
+
const claims = getValidatedIdTokenClaims(result);
|
|
1162
1337
|
if (maxAge !== skipAuthTimeCheck) {
|
|
1163
|
-
if (typeof maxAge !== 'number' || maxAge < 0) {
|
|
1164
|
-
throw new TypeError('"maxAge" must be a non-negative number');
|
|
1165
|
-
}
|
|
1166
1338
|
const now = epochTime() + getClockSkew(client);
|
|
1167
1339
|
const tolerance = getClockTolerance(client);
|
|
1168
1340
|
if (claims.auth_time + maxAge < now - tolerance) {
|
|
1169
|
-
throw
|
|
1341
|
+
throw OPE('too much time has elapsed since the last End-User authentication', JWT_TIMESTAMP_CHECK, { claims, now, tolerance, claim: 'auth_time' });
|
|
1170
1342
|
}
|
|
1171
1343
|
}
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
throw new OPE('unexpected ID Token "nonce" claim value');
|
|
1188
|
-
}
|
|
1344
|
+
if (expectedNonce === expectNoNonce) {
|
|
1345
|
+
if (claims.nonce !== undefined) {
|
|
1346
|
+
throw OPE('unexpected ID Token "nonce" claim value', JWT_CLAIM_COMPARISON, {
|
|
1347
|
+
expected: undefined,
|
|
1348
|
+
claims,
|
|
1349
|
+
claim: 'nonce',
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
else if (claims.nonce !== expectedNonce) {
|
|
1354
|
+
throw OPE('unexpected ID Token "nonce" claim value', JWT_CLAIM_COMPARISON, {
|
|
1355
|
+
expected: expectedNonce,
|
|
1356
|
+
claims,
|
|
1357
|
+
claim: 'nonce',
|
|
1358
|
+
});
|
|
1189
1359
|
}
|
|
1190
1360
|
return result;
|
|
1191
1361
|
}
|
|
1192
|
-
|
|
1193
|
-
const result = await processGenericAccessTokenResponse(as, client, response,
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1362
|
+
async function processAuthorizationCodeOAuth2Response(as, client, response, options) {
|
|
1363
|
+
const result = await processGenericAccessTokenResponse(as, client, response, undefined, options);
|
|
1364
|
+
const claims = getValidatedIdTokenClaims(result);
|
|
1365
|
+
if (claims) {
|
|
1366
|
+
if (client.default_max_age !== undefined) {
|
|
1367
|
+
assertNumber(client.default_max_age, false, '"client.default_max_age"');
|
|
1368
|
+
const now = epochTime() + getClockSkew(client);
|
|
1369
|
+
const tolerance = getClockTolerance(client);
|
|
1370
|
+
if (claims.auth_time + client.default_max_age < now - tolerance) {
|
|
1371
|
+
throw OPE('too much time has elapsed since the last End-User authentication', JWT_TIMESTAMP_CHECK, { claims, now, tolerance, claim: 'auth_time' });
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
if (claims.nonce !== undefined) {
|
|
1375
|
+
throw OPE('unexpected ID Token "nonce" claim value', JWT_CLAIM_COMPARISON, {
|
|
1376
|
+
expected: undefined,
|
|
1377
|
+
claims,
|
|
1378
|
+
claim: 'nonce',
|
|
1379
|
+
});
|
|
1200
1380
|
}
|
|
1201
|
-
delete result.id_token;
|
|
1202
1381
|
}
|
|
1203
1382
|
return result;
|
|
1204
1383
|
}
|
|
1384
|
+
export const WWW_AUTHENTICATE_CHALLENGE = 'OAUTH_WWW_AUTHENTICATE_CHALLENGE';
|
|
1385
|
+
export const RESPONSE_BODY_ERROR = 'OAUTH_RESPONSE_BODY_ERROR';
|
|
1386
|
+
export const UNSUPPORTED_OPERATION = 'OAUTH_UNSUPPORTED_OPERATION';
|
|
1387
|
+
export const AUTHORIZATION_RESPONSE_ERROR = 'OAUTH_AUTHORIZATION_RESPONSE_ERROR';
|
|
1388
|
+
export const JWT_USERINFO_EXPECTED = 'OAUTH_JWT_USERINFO_EXPECTED';
|
|
1389
|
+
export const PARSE_ERROR = 'OAUTH_PARSE_ERROR';
|
|
1390
|
+
export const INVALID_RESPONSE = 'OAUTH_INVALID_RESPONSE';
|
|
1391
|
+
export const INVALID_REQUEST = 'OAUTH_INVALID_REQUEST';
|
|
1392
|
+
export const RESPONSE_IS_NOT_JSON = 'OAUTH_RESPONSE_IS_NOT_JSON';
|
|
1393
|
+
export const RESPONSE_IS_NOT_CONFORM = 'OAUTH_RESPONSE_IS_NOT_CONFORM';
|
|
1394
|
+
export const HTTP_REQUEST_FORBIDDEN = 'OAUTH_HTTP_REQUEST_FORBIDDEN';
|
|
1395
|
+
export const REQUEST_PROTOCOL_FORBIDDEN = 'OAUTH_REQUEST_PROTOCOL_FORBIDDEN';
|
|
1396
|
+
export const JWT_TIMESTAMP_CHECK = 'OAUTH_JWT_TIMESTAMP_CHECK_FAILED';
|
|
1397
|
+
export const JWT_CLAIM_COMPARISON = 'OAUTH_JWT_CLAIM_COMPARISON_FAILED';
|
|
1398
|
+
export const JSON_ATTRIBUTE_COMPARISON = 'OAUTH_JSON_ATTRIBUTE_COMPARISON_FAILED';
|
|
1399
|
+
export const KEY_SELECTION = 'OAUTH_KEY_SELECTION_FAILED';
|
|
1400
|
+
export const MISSING_SERVER_METADATA = 'OAUTH_MISSING_SERVER_METADATA';
|
|
1401
|
+
export const INVALID_SERVER_METADATA = 'OAUTH_INVALID_SERVER_METADATA';
|
|
1205
1402
|
function checkJwtType(expected, result) {
|
|
1206
1403
|
if (typeof result.header.typ !== 'string' || normalizeTyp(result.header.typ) !== expected) {
|
|
1207
|
-
throw
|
|
1404
|
+
throw OPE('unexpected JWT "typ" header parameter value', INVALID_RESPONSE, {
|
|
1405
|
+
header: result.header,
|
|
1406
|
+
});
|
|
1208
1407
|
}
|
|
1209
1408
|
return result;
|
|
1210
1409
|
}
|
|
1211
|
-
export async function clientCredentialsGrantRequest(as, client, parameters, options) {
|
|
1410
|
+
export async function clientCredentialsGrantRequest(as, client, clientAuthentication, parameters, options) {
|
|
1212
1411
|
assertAs(as);
|
|
1213
1412
|
assertClient(client);
|
|
1214
|
-
return tokenEndpointRequest(as, client, 'client_credentials', new URLSearchParams(parameters), options);
|
|
1413
|
+
return tokenEndpointRequest(as, client, clientAuthentication, 'client_credentials', new URLSearchParams(parameters), options);
|
|
1215
1414
|
}
|
|
1216
|
-
export async function genericTokenEndpointRequest(as, client, grantType, parameters, options) {
|
|
1415
|
+
export async function genericTokenEndpointRequest(as, client, clientAuthentication, grantType, parameters, options) {
|
|
1217
1416
|
assertAs(as);
|
|
1218
1417
|
assertClient(client);
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
}
|
|
1222
|
-
return tokenEndpointRequest(as, client, grantType, new URLSearchParams(parameters), options);
|
|
1418
|
+
assertString(grantType, '"grantType"');
|
|
1419
|
+
return tokenEndpointRequest(as, client, clientAuthentication, grantType, new URLSearchParams(parameters), options);
|
|
1223
1420
|
}
|
|
1224
|
-
export async function
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
return result;
|
|
1421
|
+
export async function processGenericTokenEndpointResponse(as, client, response, options) {
|
|
1422
|
+
return processGenericAccessTokenResponse(as, client, response, undefined, options);
|
|
1423
|
+
}
|
|
1424
|
+
export async function processClientCredentialsResponse(as, client, response, options) {
|
|
1425
|
+
return processGenericAccessTokenResponse(as, client, response, undefined, options);
|
|
1230
1426
|
}
|
|
1231
|
-
export async function revocationRequest(as, client, token, options) {
|
|
1427
|
+
export async function revocationRequest(as, client, clientAuthentication, token, options) {
|
|
1232
1428
|
assertAs(as);
|
|
1233
1429
|
assertClient(client);
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
}
|
|
1237
|
-
const url = resolveEndpoint(as, 'revocation_endpoint', alias(client, options));
|
|
1430
|
+
assertString(token, '"token"');
|
|
1431
|
+
const url = resolveEndpoint(as, 'revocation_endpoint', client.use_mtls_endpoint_aliases, options?.[allowInsecureRequests] !== true);
|
|
1238
1432
|
const body = new URLSearchParams(options?.additionalParameters);
|
|
1239
1433
|
body.set('token', token);
|
|
1240
1434
|
const headers = prepareHeaders(options?.headers);
|
|
1241
1435
|
headers.delete('accept');
|
|
1242
|
-
return authenticatedRequest(as, client,
|
|
1436
|
+
return authenticatedRequest(as, client, clientAuthentication, url, body, headers, options);
|
|
1243
1437
|
}
|
|
1244
1438
|
export async function processRevocationResponse(response) {
|
|
1245
1439
|
if (!looseInstanceOf(response, Response)) {
|
|
1246
|
-
throw
|
|
1440
|
+
throw CodedTypeError('"response" must be an instance of Response', ERR_INVALID_ARG_TYPE);
|
|
1441
|
+
}
|
|
1442
|
+
let challenges;
|
|
1443
|
+
if ((challenges = parseWwwAuthenticateChallenges(response))) {
|
|
1444
|
+
throw new WWWAuthenticateChallengeError('server responded with a challenge in the WWW-Authenticate HTTP Header', { cause: challenges, response });
|
|
1247
1445
|
}
|
|
1248
1446
|
if (response.status !== 200) {
|
|
1249
1447
|
let err;
|
|
1250
1448
|
if ((err = await handleOAuthBodyError(response))) {
|
|
1251
|
-
|
|
1449
|
+
await response.body?.cancel();
|
|
1450
|
+
throw new ResponseBodyError('server responded with an error in the response body', {
|
|
1451
|
+
cause: err,
|
|
1452
|
+
response,
|
|
1453
|
+
});
|
|
1252
1454
|
}
|
|
1253
|
-
throw
|
|
1455
|
+
throw OPE('"response" is not a conform Revocation Endpoint response', RESPONSE_IS_NOT_CONFORM, response);
|
|
1254
1456
|
}
|
|
1255
1457
|
return undefined;
|
|
1256
1458
|
}
|
|
1257
1459
|
function assertReadableResponse(response) {
|
|
1258
1460
|
if (response.bodyUsed) {
|
|
1259
|
-
throw
|
|
1461
|
+
throw CodedTypeError('"response" body has been used already', ERR_INVALID_ARG_VALUE);
|
|
1260
1462
|
}
|
|
1261
1463
|
}
|
|
1262
|
-
export async function introspectionRequest(as, client, token, options) {
|
|
1464
|
+
export async function introspectionRequest(as, client, clientAuthentication, token, options) {
|
|
1263
1465
|
assertAs(as);
|
|
1264
1466
|
assertClient(client);
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
}
|
|
1268
|
-
const url = resolveEndpoint(as, 'introspection_endpoint', alias(client, options));
|
|
1467
|
+
assertString(token, '"token"');
|
|
1468
|
+
const url = resolveEndpoint(as, 'introspection_endpoint', client.use_mtls_endpoint_aliases, options?.[allowInsecureRequests] !== true);
|
|
1269
1469
|
const body = new URLSearchParams(options?.additionalParameters);
|
|
1270
1470
|
body.set('token', token);
|
|
1271
1471
|
const headers = prepareHeaders(options?.headers);
|
|
@@ -1275,109 +1475,113 @@ export async function introspectionRequest(as, client, token, options) {
|
|
|
1275
1475
|
else {
|
|
1276
1476
|
headers.set('accept', 'application/json');
|
|
1277
1477
|
}
|
|
1278
|
-
return authenticatedRequest(as, client,
|
|
1478
|
+
return authenticatedRequest(as, client, clientAuthentication, url, body, headers, options);
|
|
1279
1479
|
}
|
|
1280
|
-
export async function processIntrospectionResponse(as, client, response) {
|
|
1480
|
+
export async function processIntrospectionResponse(as, client, response, options) {
|
|
1281
1481
|
assertAs(as);
|
|
1282
1482
|
assertClient(client);
|
|
1283
1483
|
if (!looseInstanceOf(response, Response)) {
|
|
1284
|
-
throw
|
|
1484
|
+
throw CodedTypeError('"response" must be an instance of Response', ERR_INVALID_ARG_TYPE);
|
|
1485
|
+
}
|
|
1486
|
+
let challenges;
|
|
1487
|
+
if ((challenges = parseWwwAuthenticateChallenges(response))) {
|
|
1488
|
+
throw new WWWAuthenticateChallengeError('server responded with a challenge in the WWW-Authenticate HTTP Header', { cause: challenges, response });
|
|
1285
1489
|
}
|
|
1286
1490
|
if (response.status !== 200) {
|
|
1287
1491
|
let err;
|
|
1288
1492
|
if ((err = await handleOAuthBodyError(response))) {
|
|
1289
|
-
|
|
1493
|
+
await response.body?.cancel();
|
|
1494
|
+
throw new ResponseBodyError('server responded with an error in the response body', {
|
|
1495
|
+
cause: err,
|
|
1496
|
+
response,
|
|
1497
|
+
});
|
|
1290
1498
|
}
|
|
1291
|
-
throw
|
|
1499
|
+
throw OPE('"response" is not a conform Introspection Endpoint response', RESPONSE_IS_NOT_CONFORM, response);
|
|
1292
1500
|
}
|
|
1293
1501
|
let json;
|
|
1294
1502
|
if (getContentType(response) === 'application/token-introspection+jwt') {
|
|
1295
1503
|
assertReadableResponse(response);
|
|
1296
|
-
const { claims, jwt } = await validateJwt(await response.text(), checkSigningAlgorithm.bind(undefined, client.introspection_signed_response_alg, as.introspection_signing_alg_values_supported
|
|
1504
|
+
const { claims, jwt } = await validateJwt(await response.text(), checkSigningAlgorithm.bind(undefined, client.introspection_signed_response_alg, as.introspection_signing_alg_values_supported, 'RS256'), getClockSkew(client), getClockTolerance(client), options?.[jweDecrypt])
|
|
1297
1505
|
.then(checkJwtType.bind(undefined, 'token-introspection+jwt'))
|
|
1298
1506
|
.then(validatePresence.bind(undefined, ['aud', 'iat', 'iss']))
|
|
1299
|
-
.then(validateIssuer.bind(undefined, as
|
|
1507
|
+
.then(validateIssuer.bind(undefined, as))
|
|
1300
1508
|
.then(validateAudience.bind(undefined, client.client_id));
|
|
1301
|
-
|
|
1509
|
+
jwtRefs.set(response, jwt);
|
|
1302
1510
|
json = claims.token_introspection;
|
|
1303
1511
|
if (!isJsonObject(json)) {
|
|
1304
|
-
throw
|
|
1512
|
+
throw OPE('JWT "token_introspection" claim must be a JSON object', INVALID_RESPONSE, {
|
|
1513
|
+
claims,
|
|
1514
|
+
});
|
|
1305
1515
|
}
|
|
1306
1516
|
}
|
|
1307
1517
|
else {
|
|
1308
1518
|
assertReadableResponse(response);
|
|
1519
|
+
assertApplicationJson(response);
|
|
1309
1520
|
try {
|
|
1310
1521
|
json = await response.json();
|
|
1311
1522
|
}
|
|
1312
1523
|
catch (cause) {
|
|
1313
|
-
throw
|
|
1524
|
+
throw OPE('failed to parse "response" body as JSON', PARSE_ERROR, cause);
|
|
1314
1525
|
}
|
|
1315
1526
|
if (!isJsonObject(json)) {
|
|
1316
|
-
throw
|
|
1527
|
+
throw OPE('"response" body must be a top level object', INVALID_RESPONSE, { body: json });
|
|
1317
1528
|
}
|
|
1318
1529
|
}
|
|
1319
1530
|
if (typeof json.active !== 'boolean') {
|
|
1320
|
-
throw
|
|
1531
|
+
throw OPE('"response" body "active" property must be a boolean', INVALID_RESPONSE, {
|
|
1532
|
+
body: json,
|
|
1533
|
+
});
|
|
1321
1534
|
}
|
|
1322
1535
|
return json;
|
|
1323
1536
|
}
|
|
1324
1537
|
async function jwksRequest(as, options) {
|
|
1325
1538
|
assertAs(as);
|
|
1326
|
-
const url = resolveEndpoint(as, 'jwks_uri');
|
|
1539
|
+
const url = resolveEndpoint(as, 'jwks_uri', false, options?.[allowInsecureRequests] !== true);
|
|
1327
1540
|
const headers = prepareHeaders(options?.headers);
|
|
1328
1541
|
headers.set('accept', 'application/json');
|
|
1329
1542
|
headers.append('accept', 'application/jwk-set+json');
|
|
1330
1543
|
return (options?.[customFetch] || fetch)(url.href, {
|
|
1544
|
+
body: undefined,
|
|
1331
1545
|
headers: Object.fromEntries(headers.entries()),
|
|
1332
1546
|
method: 'GET',
|
|
1333
1547
|
redirect: 'manual',
|
|
1334
|
-
signal: options?.signal ? signal(options.signal) :
|
|
1335
|
-
})
|
|
1548
|
+
signal: options?.signal ? signal(options.signal) : undefined,
|
|
1549
|
+
});
|
|
1336
1550
|
}
|
|
1337
1551
|
async function processJwksResponse(response) {
|
|
1338
1552
|
if (!looseInstanceOf(response, Response)) {
|
|
1339
|
-
throw
|
|
1553
|
+
throw CodedTypeError('"response" must be an instance of Response', ERR_INVALID_ARG_TYPE);
|
|
1340
1554
|
}
|
|
1341
1555
|
if (response.status !== 200) {
|
|
1342
|
-
throw
|
|
1556
|
+
throw OPE('"response" is not a conform JSON Web Key Set response', RESPONSE_IS_NOT_CONFORM, response);
|
|
1343
1557
|
}
|
|
1344
1558
|
assertReadableResponse(response);
|
|
1559
|
+
assertContentTypes(response, 'application/json', 'application/jwk-set+json');
|
|
1345
1560
|
let json;
|
|
1346
1561
|
try {
|
|
1347
1562
|
json = await response.json();
|
|
1348
1563
|
}
|
|
1349
1564
|
catch (cause) {
|
|
1350
|
-
throw
|
|
1565
|
+
throw OPE('failed to parse "response" body as JSON', PARSE_ERROR, cause);
|
|
1351
1566
|
}
|
|
1352
1567
|
if (!isJsonObject(json)) {
|
|
1353
|
-
throw
|
|
1568
|
+
throw OPE('"response" body must be a top level object', INVALID_RESPONSE, { body: json });
|
|
1354
1569
|
}
|
|
1355
1570
|
if (!Array.isArray(json.keys)) {
|
|
1356
|
-
throw
|
|
1571
|
+
throw OPE('"response" body "keys" property must be an array', INVALID_RESPONSE, { body: json });
|
|
1357
1572
|
}
|
|
1358
1573
|
if (!Array.prototype.every.call(json.keys, isJsonObject)) {
|
|
1359
|
-
throw
|
|
1574
|
+
throw OPE('"response" body "keys" property members must be JWK formatted objects', INVALID_RESPONSE, { body: json });
|
|
1360
1575
|
}
|
|
1361
1576
|
return json;
|
|
1362
1577
|
}
|
|
1363
1578
|
async function handleOAuthBodyError(response) {
|
|
1364
1579
|
if (response.status > 399 && response.status < 500) {
|
|
1365
1580
|
assertReadableResponse(response);
|
|
1581
|
+
assertApplicationJson(response);
|
|
1366
1582
|
try {
|
|
1367
|
-
const json = await response.json();
|
|
1583
|
+
const json = await response.clone().json();
|
|
1368
1584
|
if (isJsonObject(json) && typeof json.error === 'string' && json.error.length) {
|
|
1369
|
-
if (json.error_description !== undefined && typeof json.error_description !== 'string') {
|
|
1370
|
-
delete json.error_description;
|
|
1371
|
-
}
|
|
1372
|
-
if (json.error_uri !== undefined && typeof json.error_uri !== 'string') {
|
|
1373
|
-
delete json.error_uri;
|
|
1374
|
-
}
|
|
1375
|
-
if (json.algs !== undefined && typeof json.algs !== 'string') {
|
|
1376
|
-
delete json.algs;
|
|
1377
|
-
}
|
|
1378
|
-
if (json.scope !== undefined && typeof json.scope !== 'string') {
|
|
1379
|
-
delete json.scope;
|
|
1380
|
-
}
|
|
1381
1585
|
return json;
|
|
1382
1586
|
}
|
|
1383
1587
|
}
|
|
@@ -1385,19 +1589,42 @@ async function handleOAuthBodyError(response) {
|
|
|
1385
1589
|
}
|
|
1386
1590
|
return undefined;
|
|
1387
1591
|
}
|
|
1388
|
-
function
|
|
1389
|
-
|
|
1390
|
-
|
|
1592
|
+
function supported(alg) {
|
|
1593
|
+
switch (alg) {
|
|
1594
|
+
case 'PS256':
|
|
1595
|
+
case 'ES256':
|
|
1596
|
+
case 'RS256':
|
|
1597
|
+
case 'PS384':
|
|
1598
|
+
case 'ES384':
|
|
1599
|
+
case 'RS384':
|
|
1600
|
+
case 'PS512':
|
|
1601
|
+
case 'ES512':
|
|
1602
|
+
case 'RS512':
|
|
1603
|
+
case 'Ed25519':
|
|
1604
|
+
case 'EdDSA':
|
|
1605
|
+
return true;
|
|
1606
|
+
default:
|
|
1607
|
+
return false;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
function checkSupportedJwsAlg(header) {
|
|
1611
|
+
if (!supported(header.alg)) {
|
|
1612
|
+
throw new UnsupportedOperationError('unsupported JWS "alg" identifier', {
|
|
1613
|
+
cause: { alg: header.alg },
|
|
1614
|
+
});
|
|
1391
1615
|
}
|
|
1392
|
-
return alg;
|
|
1393
1616
|
}
|
|
1394
|
-
function checkRsaKeyAlgorithm(
|
|
1617
|
+
function checkRsaKeyAlgorithm(key) {
|
|
1618
|
+
const { algorithm } = key;
|
|
1395
1619
|
if (typeof algorithm.modulusLength !== 'number' || algorithm.modulusLength < 2048) {
|
|
1396
|
-
throw new
|
|
1620
|
+
throw new UnsupportedOperationError(`unsupported ${algorithm.name} modulusLength`, {
|
|
1621
|
+
cause: key,
|
|
1622
|
+
});
|
|
1397
1623
|
}
|
|
1398
1624
|
}
|
|
1399
|
-
function ecdsaHashName(
|
|
1400
|
-
|
|
1625
|
+
function ecdsaHashName(key) {
|
|
1626
|
+
const { algorithm } = key;
|
|
1627
|
+
switch (algorithm.namedCurve) {
|
|
1401
1628
|
case 'P-256':
|
|
1402
1629
|
return 'SHA-256';
|
|
1403
1630
|
case 'P-384':
|
|
@@ -1405,7 +1632,7 @@ function ecdsaHashName(namedCurve) {
|
|
|
1405
1632
|
case 'P-521':
|
|
1406
1633
|
return 'SHA-512';
|
|
1407
1634
|
default:
|
|
1408
|
-
throw new UnsupportedOperationError();
|
|
1635
|
+
throw new UnsupportedOperationError('unsupported ECDSA namedCurve', { cause: key });
|
|
1409
1636
|
}
|
|
1410
1637
|
}
|
|
1411
1638
|
function keyToSubtle(key) {
|
|
@@ -1413,10 +1640,10 @@ function keyToSubtle(key) {
|
|
|
1413
1640
|
case 'ECDSA':
|
|
1414
1641
|
return {
|
|
1415
1642
|
name: key.algorithm.name,
|
|
1416
|
-
hash: ecdsaHashName(key
|
|
1643
|
+
hash: ecdsaHashName(key),
|
|
1417
1644
|
};
|
|
1418
1645
|
case 'RSA-PSS': {
|
|
1419
|
-
checkRsaKeyAlgorithm(key
|
|
1646
|
+
checkRsaKeyAlgorithm(key);
|
|
1420
1647
|
switch (key.algorithm.hash.name) {
|
|
1421
1648
|
case 'SHA-256':
|
|
1422
1649
|
case 'SHA-384':
|
|
@@ -1426,103 +1653,109 @@ function keyToSubtle(key) {
|
|
|
1426
1653
|
saltLength: parseInt(key.algorithm.hash.name.slice(-3), 10) >> 3,
|
|
1427
1654
|
};
|
|
1428
1655
|
default:
|
|
1429
|
-
throw new UnsupportedOperationError();
|
|
1656
|
+
throw new UnsupportedOperationError('unsupported RSA-PSS hash name', { cause: key });
|
|
1430
1657
|
}
|
|
1431
1658
|
}
|
|
1432
1659
|
case 'RSASSA-PKCS1-v1_5':
|
|
1433
|
-
checkRsaKeyAlgorithm(key
|
|
1660
|
+
checkRsaKeyAlgorithm(key);
|
|
1434
1661
|
return key.algorithm.name;
|
|
1435
|
-
case 'Ed448':
|
|
1436
1662
|
case 'Ed25519':
|
|
1663
|
+
case 'EdDSA':
|
|
1437
1664
|
return key.algorithm.name;
|
|
1438
1665
|
}
|
|
1439
|
-
throw new UnsupportedOperationError();
|
|
1666
|
+
throw new UnsupportedOperationError('unsupported CryptoKey algorithm name', { cause: key });
|
|
1440
1667
|
}
|
|
1441
|
-
const noSignatureCheck = Symbol();
|
|
1442
1668
|
async function validateJwsSignature(protectedHeader, payload, key, signature) {
|
|
1443
|
-
const
|
|
1444
|
-
const
|
|
1669
|
+
const data = buf(`${protectedHeader}.${payload}`);
|
|
1670
|
+
const algorithm = keyToSubtle(key);
|
|
1671
|
+
const verified = await crypto.subtle.verify(algorithm, key, signature, data);
|
|
1445
1672
|
if (!verified) {
|
|
1446
|
-
throw
|
|
1673
|
+
throw OPE('JWT signature verification failed', INVALID_RESPONSE, {
|
|
1674
|
+
key,
|
|
1675
|
+
data,
|
|
1676
|
+
signature,
|
|
1677
|
+
algorithm,
|
|
1678
|
+
});
|
|
1447
1679
|
}
|
|
1448
1680
|
}
|
|
1449
|
-
async function validateJwt(jws, checkAlg,
|
|
1450
|
-
let { 0: protectedHeader, 1: payload,
|
|
1681
|
+
async function validateJwt(jws, checkAlg, clockSkew, clockTolerance, decryptJwt) {
|
|
1682
|
+
let { 0: protectedHeader, 1: payload, length } = jws.split('.');
|
|
1451
1683
|
if (length === 5) {
|
|
1452
1684
|
if (decryptJwt !== undefined) {
|
|
1453
1685
|
jws = await decryptJwt(jws);
|
|
1454
|
-
({ 0: protectedHeader, 1: payload,
|
|
1686
|
+
({ 0: protectedHeader, 1: payload, length } = jws.split('.'));
|
|
1455
1687
|
}
|
|
1456
1688
|
else {
|
|
1457
|
-
throw new UnsupportedOperationError('JWE
|
|
1689
|
+
throw new UnsupportedOperationError('JWE decryption is not configured', { cause: jws });
|
|
1458
1690
|
}
|
|
1459
1691
|
}
|
|
1460
1692
|
if (length !== 3) {
|
|
1461
|
-
throw
|
|
1693
|
+
throw OPE('Invalid JWT', INVALID_RESPONSE, jws);
|
|
1462
1694
|
}
|
|
1463
1695
|
let header;
|
|
1464
1696
|
try {
|
|
1465
1697
|
header = JSON.parse(buf(b64u(protectedHeader)));
|
|
1466
1698
|
}
|
|
1467
1699
|
catch (cause) {
|
|
1468
|
-
throw
|
|
1700
|
+
throw OPE('failed to parse JWT Header body as base64url encoded JSON', PARSE_ERROR, cause);
|
|
1469
1701
|
}
|
|
1470
1702
|
if (!isJsonObject(header)) {
|
|
1471
|
-
throw
|
|
1703
|
+
throw OPE('JWT Header must be a top level object', INVALID_RESPONSE, jws);
|
|
1472
1704
|
}
|
|
1473
1705
|
checkAlg(header);
|
|
1474
1706
|
if (header.crit !== undefined) {
|
|
1475
|
-
throw new
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
let key;
|
|
1479
|
-
if (getKey !== noSignatureCheck) {
|
|
1480
|
-
key = await getKey(header);
|
|
1481
|
-
await validateJwsSignature(protectedHeader, payload, key, signature);
|
|
1707
|
+
throw new UnsupportedOperationError('no JWT "crit" header parameter extensions are supported', {
|
|
1708
|
+
cause: { header },
|
|
1709
|
+
});
|
|
1482
1710
|
}
|
|
1483
1711
|
let claims;
|
|
1484
1712
|
try {
|
|
1485
1713
|
claims = JSON.parse(buf(b64u(payload)));
|
|
1486
1714
|
}
|
|
1487
1715
|
catch (cause) {
|
|
1488
|
-
throw
|
|
1716
|
+
throw OPE('failed to parse JWT Payload body as base64url encoded JSON', PARSE_ERROR, cause);
|
|
1489
1717
|
}
|
|
1490
1718
|
if (!isJsonObject(claims)) {
|
|
1491
|
-
throw
|
|
1719
|
+
throw OPE('JWT Payload must be a top level object', INVALID_RESPONSE, jws);
|
|
1492
1720
|
}
|
|
1493
1721
|
const now = epochTime() + clockSkew;
|
|
1494
1722
|
if (claims.exp !== undefined) {
|
|
1495
1723
|
if (typeof claims.exp !== 'number') {
|
|
1496
|
-
throw
|
|
1724
|
+
throw OPE('unexpected JWT "exp" (expiration time) claim type', INVALID_RESPONSE, { claims });
|
|
1497
1725
|
}
|
|
1498
1726
|
if (claims.exp <= now - clockTolerance) {
|
|
1499
|
-
throw
|
|
1727
|
+
throw OPE('unexpected JWT "exp" (expiration time) claim value, expiration is past current timestamp', JWT_TIMESTAMP_CHECK, { claims, now, tolerance: clockTolerance, claim: 'exp' });
|
|
1500
1728
|
}
|
|
1501
1729
|
}
|
|
1502
1730
|
if (claims.iat !== undefined) {
|
|
1503
1731
|
if (typeof claims.iat !== 'number') {
|
|
1504
|
-
throw
|
|
1732
|
+
throw OPE('unexpected JWT "iat" (issued at) claim type', INVALID_RESPONSE, { claims });
|
|
1505
1733
|
}
|
|
1506
1734
|
}
|
|
1507
1735
|
if (claims.iss !== undefined) {
|
|
1508
1736
|
if (typeof claims.iss !== 'string') {
|
|
1509
|
-
throw
|
|
1737
|
+
throw OPE('unexpected JWT "iss" (issuer) claim type', INVALID_RESPONSE, { claims });
|
|
1510
1738
|
}
|
|
1511
1739
|
}
|
|
1512
1740
|
if (claims.nbf !== undefined) {
|
|
1513
1741
|
if (typeof claims.nbf !== 'number') {
|
|
1514
|
-
throw
|
|
1742
|
+
throw OPE('unexpected JWT "nbf" (not before) claim type', INVALID_RESPONSE, { claims });
|
|
1515
1743
|
}
|
|
1516
1744
|
if (claims.nbf > now + clockTolerance) {
|
|
1517
|
-
throw
|
|
1745
|
+
throw OPE('unexpected JWT "nbf" (not before) claim value', JWT_TIMESTAMP_CHECK, {
|
|
1746
|
+
claims,
|
|
1747
|
+
now,
|
|
1748
|
+
tolerance: clockTolerance,
|
|
1749
|
+
claim: 'nbf',
|
|
1750
|
+
});
|
|
1518
1751
|
}
|
|
1519
1752
|
}
|
|
1520
1753
|
if (claims.aud !== undefined) {
|
|
1521
1754
|
if (typeof claims.aud !== 'string' && !Array.isArray(claims.aud)) {
|
|
1522
|
-
throw
|
|
1755
|
+
throw OPE('unexpected JWT "aud" (audience) claim type', INVALID_RESPONSE, { claims });
|
|
1523
1756
|
}
|
|
1524
1757
|
}
|
|
1525
|
-
return { header, claims,
|
|
1758
|
+
return { header, claims, jwt: jws };
|
|
1526
1759
|
}
|
|
1527
1760
|
export async function validateJwtAuthResponse(as, client, parameters, expectedState, options) {
|
|
1528
1761
|
assertAs(as);
|
|
@@ -1531,16 +1764,20 @@ export async function validateJwtAuthResponse(as, client, parameters, expectedSt
|
|
|
1531
1764
|
parameters = parameters.searchParams;
|
|
1532
1765
|
}
|
|
1533
1766
|
if (!(parameters instanceof URLSearchParams)) {
|
|
1534
|
-
throw
|
|
1767
|
+
throw CodedTypeError('"parameters" must be an instance of URLSearchParams, or URL', ERR_INVALID_ARG_TYPE);
|
|
1535
1768
|
}
|
|
1536
1769
|
const response = getURLSearchParameter(parameters, 'response');
|
|
1537
1770
|
if (!response) {
|
|
1538
|
-
throw
|
|
1771
|
+
throw OPE('"parameters" does not contain a JARM response', INVALID_RESPONSE);
|
|
1539
1772
|
}
|
|
1540
|
-
const { claims } = await validateJwt(response, checkSigningAlgorithm.bind(undefined, client.authorization_signed_response_alg, as.authorization_signing_alg_values_supported
|
|
1773
|
+
const { claims, header, jwt } = await validateJwt(response, checkSigningAlgorithm.bind(undefined, client.authorization_signed_response_alg, as.authorization_signing_alg_values_supported, 'RS256'), getClockSkew(client), getClockTolerance(client), options?.[jweDecrypt])
|
|
1541
1774
|
.then(validatePresence.bind(undefined, ['aud', 'exp', 'iss']))
|
|
1542
|
-
.then(validateIssuer.bind(undefined, as
|
|
1775
|
+
.then(validateIssuer.bind(undefined, as))
|
|
1543
1776
|
.then(validateAudience.bind(undefined, client.client_id));
|
|
1777
|
+
const { 0: protectedHeader, 1: payload, 2: encodedSignature } = jwt.split('.');
|
|
1778
|
+
const signature = b64u(encodedSignature);
|
|
1779
|
+
const key = await getPublicSigKeyFromIssuerJwksUri(as, options, header);
|
|
1780
|
+
await validateJwsSignature(protectedHeader, payload, key, signature);
|
|
1544
1781
|
const result = new URLSearchParams();
|
|
1545
1782
|
for (const [key, value] of Object.entries(claims)) {
|
|
1546
1783
|
if (typeof value === 'string' && key !== 'aud') {
|
|
@@ -1549,9 +1786,9 @@ export async function validateJwtAuthResponse(as, client, parameters, expectedSt
|
|
|
1549
1786
|
}
|
|
1550
1787
|
return validateAuthResponse(as, client, result, expectedState);
|
|
1551
1788
|
}
|
|
1552
|
-
async function idTokenHash(
|
|
1789
|
+
async function idTokenHash(data, header, claimName) {
|
|
1553
1790
|
let algorithm;
|
|
1554
|
-
switch (alg) {
|
|
1791
|
+
switch (header.alg) {
|
|
1555
1792
|
case 'RS256':
|
|
1556
1793
|
case 'PS256':
|
|
1557
1794
|
case 'ES256':
|
|
@@ -1565,35 +1802,37 @@ async function idTokenHash(alg, data, key) {
|
|
|
1565
1802
|
case 'RS512':
|
|
1566
1803
|
case 'PS512':
|
|
1567
1804
|
case 'ES512':
|
|
1805
|
+
case 'Ed25519':
|
|
1806
|
+
case 'EdDSA':
|
|
1568
1807
|
algorithm = 'SHA-512';
|
|
1569
1808
|
break;
|
|
1570
|
-
case 'EdDSA':
|
|
1571
|
-
if (key.algorithm.name === 'Ed25519') {
|
|
1572
|
-
algorithm = 'SHA-512';
|
|
1573
|
-
break;
|
|
1574
|
-
}
|
|
1575
|
-
throw new UnsupportedOperationError();
|
|
1576
1809
|
default:
|
|
1577
|
-
throw new UnsupportedOperationError();
|
|
1810
|
+
throw new UnsupportedOperationError(`unsupported JWS algorithm for ${claimName} calculation`, { cause: { alg: header.alg } });
|
|
1578
1811
|
}
|
|
1579
1812
|
const digest = await crypto.subtle.digest(algorithm, buf(data));
|
|
1580
1813
|
return b64u(digest.slice(0, digest.byteLength / 2));
|
|
1581
1814
|
}
|
|
1582
|
-
async function idTokenHashMatches(data, actual,
|
|
1583
|
-
const expected = await idTokenHash(
|
|
1815
|
+
async function idTokenHashMatches(data, actual, header, claimName) {
|
|
1816
|
+
const expected = await idTokenHash(data, header, claimName);
|
|
1584
1817
|
return actual === expected;
|
|
1585
1818
|
}
|
|
1586
1819
|
export async function validateDetachedSignatureResponse(as, client, parameters, expectedNonce, expectedState, maxAge, options) {
|
|
1820
|
+
return validateHybridResponse(as, client, parameters, expectedNonce, expectedState, maxAge, options, true);
|
|
1821
|
+
}
|
|
1822
|
+
export async function validateCodeIdTokenResponse(as, client, parameters, expectedNonce, expectedState, maxAge, options) {
|
|
1823
|
+
return validateHybridResponse(as, client, parameters, expectedNonce, expectedState, maxAge, options, false);
|
|
1824
|
+
}
|
|
1825
|
+
async function validateHybridResponse(as, client, parameters, expectedNonce, expectedState, maxAge, options, fapi) {
|
|
1587
1826
|
assertAs(as);
|
|
1588
1827
|
assertClient(client);
|
|
1589
1828
|
if (parameters instanceof URL) {
|
|
1590
1829
|
if (!parameters.hash.length) {
|
|
1591
|
-
throw
|
|
1830
|
+
throw CodedTypeError('"parameters" as an instance of URL must contain a hash (fragment) with the Authorization Response parameters', ERR_INVALID_ARG_VALUE);
|
|
1592
1831
|
}
|
|
1593
1832
|
parameters = new URLSearchParams(parameters.hash.slice(1));
|
|
1594
1833
|
}
|
|
1595
1834
|
if (!(parameters instanceof URLSearchParams)) {
|
|
1596
|
-
throw
|
|
1835
|
+
throw CodedTypeError('"parameters" must be an instance of URLSearchParams', ERR_INVALID_ARG_TYPE);
|
|
1597
1836
|
}
|
|
1598
1837
|
parameters = new URLSearchParams(parameters);
|
|
1599
1838
|
const id_token = getURLSearchParameter(parameters, 'id_token');
|
|
@@ -1603,23 +1842,18 @@ export async function validateDetachedSignatureResponse(as, client, parameters,
|
|
|
1603
1842
|
case expectNoState:
|
|
1604
1843
|
break;
|
|
1605
1844
|
default:
|
|
1606
|
-
|
|
1607
|
-
throw new TypeError('"expectedState" must be a non-empty string');
|
|
1608
|
-
}
|
|
1845
|
+
assertString(expectedState, '"expectedState" argument');
|
|
1609
1846
|
}
|
|
1610
1847
|
const result = validateAuthResponse({
|
|
1611
1848
|
...as,
|
|
1612
1849
|
authorization_response_iss_parameter_supported: false,
|
|
1613
1850
|
}, client, parameters, expectedState);
|
|
1614
|
-
if (isOAuth2Error(result)) {
|
|
1615
|
-
return result;
|
|
1616
|
-
}
|
|
1617
1851
|
if (!id_token) {
|
|
1618
|
-
throw
|
|
1852
|
+
throw OPE('"parameters" does not contain an ID Token', INVALID_RESPONSE);
|
|
1619
1853
|
}
|
|
1620
1854
|
const code = getURLSearchParameter(parameters, 'code');
|
|
1621
1855
|
if (!code) {
|
|
1622
|
-
throw
|
|
1856
|
+
throw OPE('"parameters" does not contain an Authorization Code', INVALID_RESPONSE);
|
|
1623
1857
|
}
|
|
1624
1858
|
const requiredClaims = [
|
|
1625
1859
|
'aud',
|
|
@@ -1630,86 +1864,132 @@ export async function validateDetachedSignatureResponse(as, client, parameters,
|
|
|
1630
1864
|
'nonce',
|
|
1631
1865
|
'c_hash',
|
|
1632
1866
|
];
|
|
1633
|
-
|
|
1867
|
+
const state = parameters.get('state');
|
|
1868
|
+
if (fapi && (typeof expectedState === 'string' || state !== null)) {
|
|
1634
1869
|
requiredClaims.push('s_hash');
|
|
1635
1870
|
}
|
|
1636
|
-
|
|
1871
|
+
if (maxAge !== undefined) {
|
|
1872
|
+
assertNumber(maxAge, false, '"maxAge" argument');
|
|
1873
|
+
}
|
|
1874
|
+
else if (client.default_max_age !== undefined) {
|
|
1875
|
+
assertNumber(client.default_max_age, false, '"client.default_max_age"');
|
|
1876
|
+
}
|
|
1877
|
+
maxAge ??= client.default_max_age ?? skipAuthTimeCheck;
|
|
1878
|
+
if (client.require_auth_time || maxAge !== skipAuthTimeCheck) {
|
|
1879
|
+
requiredClaims.push('auth_time');
|
|
1880
|
+
}
|
|
1881
|
+
const { claims, header, jwt } = await validateJwt(id_token, checkSigningAlgorithm.bind(undefined, client.id_token_signed_response_alg, as.id_token_signing_alg_values_supported, 'RS256'), getClockSkew(client), getClockTolerance(client), options?.[jweDecrypt])
|
|
1637
1882
|
.then(validatePresence.bind(undefined, requiredClaims))
|
|
1638
|
-
.then(validateIssuer.bind(undefined, as
|
|
1883
|
+
.then(validateIssuer.bind(undefined, as))
|
|
1639
1884
|
.then(validateAudience.bind(undefined, client.client_id));
|
|
1640
1885
|
const clockSkew = getClockSkew(client);
|
|
1641
1886
|
const now = epochTime() + clockSkew;
|
|
1642
1887
|
if (claims.iat < now - 3600) {
|
|
1643
|
-
throw
|
|
1888
|
+
throw OPE('unexpected JWT "iat" (issued at) claim value, it is too far in the past', JWT_TIMESTAMP_CHECK, { now, claims, claim: 'iat' });
|
|
1644
1889
|
}
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
throw new OPE('could not verify ID Token "s_hash" (state hash) claim value');
|
|
1651
|
-
}
|
|
1652
|
-
if (typeof expectedState === 'string' &&
|
|
1653
|
-
(typeof claims.s_hash !== 'string' ||
|
|
1654
|
-
(await idTokenHashMatches(expectedState, claims.s_hash, header.alg, key)) !== true)) {
|
|
1655
|
-
throw new OPE('invalid ID Token "s_hash" (state hash) claim value');
|
|
1656
|
-
}
|
|
1657
|
-
if (claims.auth_time !== undefined &&
|
|
1658
|
-
(!Number.isFinite(claims.auth_time) || Math.sign(claims.auth_time) !== 1)) {
|
|
1659
|
-
throw new OPE('ID Token "auth_time" (authentication time) must be a positive number');
|
|
1660
|
-
}
|
|
1661
|
-
maxAge ?? (maxAge = client.default_max_age ?? skipAuthTimeCheck);
|
|
1662
|
-
if ((client.require_auth_time || maxAge !== skipAuthTimeCheck) &&
|
|
1663
|
-
claims.auth_time === undefined) {
|
|
1664
|
-
throw new OPE('ID Token "auth_time" (authentication time) claim missing');
|
|
1890
|
+
assertString(claims.c_hash, 'ID Token "c_hash" (code hash) claim value', INVALID_RESPONSE, {
|
|
1891
|
+
claims,
|
|
1892
|
+
});
|
|
1893
|
+
if (claims.auth_time !== undefined) {
|
|
1894
|
+
assertNumber(claims.auth_time, false, 'ID Token "auth_time" (authentication time)', INVALID_RESPONSE, { claims });
|
|
1665
1895
|
}
|
|
1666
1896
|
if (maxAge !== skipAuthTimeCheck) {
|
|
1667
|
-
if (typeof maxAge !== 'number' || maxAge < 0) {
|
|
1668
|
-
throw new TypeError('"maxAge" must be a non-negative number');
|
|
1669
|
-
}
|
|
1670
1897
|
const now = epochTime() + getClockSkew(client);
|
|
1671
1898
|
const tolerance = getClockTolerance(client);
|
|
1672
1899
|
if (claims.auth_time + maxAge < now - tolerance) {
|
|
1673
|
-
throw
|
|
1900
|
+
throw OPE('too much time has elapsed since the last End-User authentication', JWT_TIMESTAMP_CHECK, { claims, now, tolerance, claim: 'auth_time' });
|
|
1674
1901
|
}
|
|
1675
1902
|
}
|
|
1676
|
-
|
|
1677
|
-
throw new TypeError('"expectedNonce" must be a non-empty string');
|
|
1678
|
-
}
|
|
1903
|
+
assertString(expectedNonce, '"expectedNonce" argument');
|
|
1679
1904
|
if (claims.nonce !== expectedNonce) {
|
|
1680
|
-
throw
|
|
1905
|
+
throw OPE('unexpected ID Token "nonce" claim value', JWT_CLAIM_COMPARISON, {
|
|
1906
|
+
expected: expectedNonce,
|
|
1907
|
+
claims,
|
|
1908
|
+
claim: 'nonce',
|
|
1909
|
+
});
|
|
1681
1910
|
}
|
|
1682
1911
|
if (Array.isArray(claims.aud) && claims.aud.length !== 1) {
|
|
1683
1912
|
if (claims.azp === undefined) {
|
|
1684
|
-
throw
|
|
1913
|
+
throw OPE('ID Token "aud" (audience) claim includes additional untrusted audiences', JWT_CLAIM_COMPARISON, { claims, claim: 'aud' });
|
|
1685
1914
|
}
|
|
1686
1915
|
if (claims.azp !== client.client_id) {
|
|
1687
|
-
throw
|
|
1916
|
+
throw OPE('unexpected ID Token "azp" (authorized party) claim value', JWT_CLAIM_COMPARISON, {
|
|
1917
|
+
expected: client.client_id,
|
|
1918
|
+
claims,
|
|
1919
|
+
claim: 'azp',
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
const { 0: protectedHeader, 1: payload, 2: encodedSignature } = jwt.split('.');
|
|
1924
|
+
const signature = b64u(encodedSignature);
|
|
1925
|
+
const key = await getPublicSigKeyFromIssuerJwksUri(as, options, header);
|
|
1926
|
+
await validateJwsSignature(protectedHeader, payload, key, signature);
|
|
1927
|
+
if ((await idTokenHashMatches(code, claims.c_hash, header, 'c_hash')) !== true) {
|
|
1928
|
+
throw OPE('invalid ID Token "c_hash" (code hash) claim value', JWT_CLAIM_COMPARISON, {
|
|
1929
|
+
code,
|
|
1930
|
+
alg: header.alg,
|
|
1931
|
+
claim: 'c_hash',
|
|
1932
|
+
claims,
|
|
1933
|
+
});
|
|
1934
|
+
}
|
|
1935
|
+
if ((fapi && state !== null) || claims.s_hash !== undefined) {
|
|
1936
|
+
assertString(claims.s_hash, 'ID Token "s_hash" (state hash) claim value', INVALID_RESPONSE, {
|
|
1937
|
+
claims,
|
|
1938
|
+
});
|
|
1939
|
+
assertString(state, '"state" response parameter', INVALID_RESPONSE, { parameters });
|
|
1940
|
+
if ((await idTokenHashMatches(state, claims.s_hash, header, 's_hash')) !== true) {
|
|
1941
|
+
throw OPE('invalid ID Token "s_hash" (state hash) claim value', JWT_CLAIM_COMPARISON, {
|
|
1942
|
+
state,
|
|
1943
|
+
alg: header.alg,
|
|
1944
|
+
claim: 's_hash',
|
|
1945
|
+
claims,
|
|
1946
|
+
});
|
|
1688
1947
|
}
|
|
1689
1948
|
}
|
|
1690
1949
|
return result;
|
|
1691
1950
|
}
|
|
1692
|
-
|
|
1951
|
+
const SUPPORTED_JWS_ALGS = Symbol();
|
|
1952
|
+
function checkSigningAlgorithm(client, issuer, fallback, header) {
|
|
1693
1953
|
if (client !== undefined) {
|
|
1694
|
-
if (header.alg !== client) {
|
|
1695
|
-
throw
|
|
1954
|
+
if (typeof client === 'string' ? header.alg !== client : !client.includes(header.alg)) {
|
|
1955
|
+
throw OPE('unexpected JWT "alg" header parameter', INVALID_RESPONSE, {
|
|
1956
|
+
header,
|
|
1957
|
+
expected: client,
|
|
1958
|
+
reason: 'client configuration',
|
|
1959
|
+
});
|
|
1696
1960
|
}
|
|
1697
1961
|
return;
|
|
1698
1962
|
}
|
|
1699
1963
|
if (Array.isArray(issuer)) {
|
|
1700
1964
|
if (!issuer.includes(header.alg)) {
|
|
1701
|
-
throw
|
|
1965
|
+
throw OPE('unexpected JWT "alg" header parameter', INVALID_RESPONSE, {
|
|
1966
|
+
header,
|
|
1967
|
+
expected: issuer,
|
|
1968
|
+
reason: 'authorization server metadata',
|
|
1969
|
+
});
|
|
1702
1970
|
}
|
|
1703
1971
|
return;
|
|
1704
1972
|
}
|
|
1705
|
-
if (
|
|
1706
|
-
|
|
1973
|
+
if (fallback !== undefined) {
|
|
1974
|
+
if (typeof fallback === 'string'
|
|
1975
|
+
? header.alg !== fallback
|
|
1976
|
+
: typeof fallback === 'symbol'
|
|
1977
|
+
? !supported(header.alg)
|
|
1978
|
+
: !fallback.includes(header.alg)) {
|
|
1979
|
+
throw OPE('unexpected JWT "alg" header parameter', INVALID_RESPONSE, {
|
|
1980
|
+
header,
|
|
1981
|
+
expected: fallback,
|
|
1982
|
+
reason: 'default value',
|
|
1983
|
+
});
|
|
1984
|
+
}
|
|
1985
|
+
return;
|
|
1707
1986
|
}
|
|
1987
|
+
throw OPE('missing client or server configuration to verify used JWT "alg" header parameter', undefined, { client, issuer, fallback });
|
|
1708
1988
|
}
|
|
1709
1989
|
function getURLSearchParameter(parameters, name) {
|
|
1710
1990
|
const { 0: value, length } = parameters.getAll(name);
|
|
1711
1991
|
if (length > 1) {
|
|
1712
|
-
throw
|
|
1992
|
+
throw OPE(`"${name}" parameter must be provided only once`, INVALID_RESPONSE);
|
|
1713
1993
|
}
|
|
1714
1994
|
return value;
|
|
1715
1995
|
}
|
|
@@ -1722,46 +2002,47 @@ export function validateAuthResponse(as, client, parameters, expectedState) {
|
|
|
1722
2002
|
parameters = parameters.searchParams;
|
|
1723
2003
|
}
|
|
1724
2004
|
if (!(parameters instanceof URLSearchParams)) {
|
|
1725
|
-
throw
|
|
2005
|
+
throw CodedTypeError('"parameters" must be an instance of URLSearchParams, or URL', ERR_INVALID_ARG_TYPE);
|
|
1726
2006
|
}
|
|
1727
2007
|
if (getURLSearchParameter(parameters, 'response')) {
|
|
1728
|
-
throw
|
|
2008
|
+
throw OPE('"parameters" contains a JARM response, use validateJwtAuthResponse() instead of validateAuthResponse()', INVALID_RESPONSE, { parameters });
|
|
1729
2009
|
}
|
|
1730
2010
|
const iss = getURLSearchParameter(parameters, 'iss');
|
|
1731
2011
|
const state = getURLSearchParameter(parameters, 'state');
|
|
1732
2012
|
if (!iss && as.authorization_response_iss_parameter_supported) {
|
|
1733
|
-
throw
|
|
2013
|
+
throw OPE('response parameter "iss" (issuer) missing', INVALID_RESPONSE, { parameters });
|
|
1734
2014
|
}
|
|
1735
2015
|
if (iss && iss !== as.issuer) {
|
|
1736
|
-
throw
|
|
2016
|
+
throw OPE('unexpected "iss" (issuer) response parameter value', INVALID_RESPONSE, {
|
|
2017
|
+
expected: as.issuer,
|
|
2018
|
+
parameters,
|
|
2019
|
+
});
|
|
1737
2020
|
}
|
|
1738
2021
|
switch (expectedState) {
|
|
1739
2022
|
case undefined:
|
|
1740
2023
|
case expectNoState:
|
|
1741
2024
|
if (state !== undefined) {
|
|
1742
|
-
throw
|
|
2025
|
+
throw OPE('unexpected "state" response parameter encountered', INVALID_RESPONSE, {
|
|
2026
|
+
expected: undefined,
|
|
2027
|
+
parameters,
|
|
2028
|
+
});
|
|
1743
2029
|
}
|
|
1744
2030
|
break;
|
|
1745
2031
|
case skipStateCheck:
|
|
1746
2032
|
break;
|
|
1747
2033
|
default:
|
|
1748
|
-
|
|
1749
|
-
throw new OPE('"expectedState" must be a non-empty string');
|
|
1750
|
-
}
|
|
1751
|
-
if (state === undefined) {
|
|
1752
|
-
throw new OPE('response parameter "state" missing');
|
|
1753
|
-
}
|
|
2034
|
+
assertString(expectedState, '"expectedState" argument');
|
|
1754
2035
|
if (state !== expectedState) {
|
|
1755
|
-
throw
|
|
2036
|
+
throw OPE(state === undefined
|
|
2037
|
+
? 'response parameter "state" missing'
|
|
2038
|
+
: 'unexpected "state" response parameter value', INVALID_RESPONSE, { expected: expectedState, parameters });
|
|
1756
2039
|
}
|
|
1757
2040
|
}
|
|
1758
2041
|
const error = getURLSearchParameter(parameters, 'error');
|
|
1759
2042
|
if (error) {
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
error_uri: getURLSearchParameter(parameters, 'error_uri'),
|
|
1764
|
-
};
|
|
2043
|
+
throw new AuthorizationResponseError('authorization response from the server is an error', {
|
|
2044
|
+
cause: parameters,
|
|
2045
|
+
});
|
|
1765
2046
|
}
|
|
1766
2047
|
const id_token = getURLSearchParameter(parameters, 'id_token');
|
|
1767
2048
|
const token = getURLSearchParameter(parameters, 'token');
|
|
@@ -1770,7 +2051,7 @@ export function validateAuthResponse(as, client, parameters, expectedState) {
|
|
|
1770
2051
|
}
|
|
1771
2052
|
return brand(new URLSearchParams(parameters));
|
|
1772
2053
|
}
|
|
1773
|
-
function algToSubtle(alg
|
|
2054
|
+
function algToSubtle(alg) {
|
|
1774
2055
|
switch (alg) {
|
|
1775
2056
|
case 'PS256':
|
|
1776
2057
|
case 'PS384':
|
|
@@ -1785,96 +2066,96 @@ function algToSubtle(alg, crv) {
|
|
|
1785
2066
|
return { name: 'ECDSA', namedCurve: `P-${alg.slice(-3)}` };
|
|
1786
2067
|
case 'ES512':
|
|
1787
2068
|
return { name: 'ECDSA', namedCurve: 'P-521' };
|
|
1788
|
-
case '
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
case 'Ed448':
|
|
1792
|
-
return crv;
|
|
1793
|
-
default:
|
|
1794
|
-
throw new UnsupportedOperationError();
|
|
1795
|
-
}
|
|
1796
|
-
}
|
|
2069
|
+
case 'Ed25519':
|
|
2070
|
+
case 'EdDSA':
|
|
2071
|
+
return 'Ed25519';
|
|
1797
2072
|
default:
|
|
1798
|
-
throw new UnsupportedOperationError();
|
|
2073
|
+
throw new UnsupportedOperationError('unsupported JWS algorithm', { cause: { alg } });
|
|
1799
2074
|
}
|
|
1800
2075
|
}
|
|
1801
2076
|
async function importJwk(alg, jwk) {
|
|
1802
2077
|
const { ext, key_ops, use, ...key } = jwk;
|
|
1803
|
-
return crypto.subtle.importKey('jwk', key, algToSubtle(alg
|
|
2078
|
+
return crypto.subtle.importKey('jwk', key, algToSubtle(alg), true, ['verify']);
|
|
1804
2079
|
}
|
|
1805
|
-
export async function deviceAuthorizationRequest(as, client, parameters, options) {
|
|
2080
|
+
export async function deviceAuthorizationRequest(as, client, clientAuthentication, parameters, options) {
|
|
1806
2081
|
assertAs(as);
|
|
1807
2082
|
assertClient(client);
|
|
1808
|
-
const url = resolveEndpoint(as, 'device_authorization_endpoint',
|
|
2083
|
+
const url = resolveEndpoint(as, 'device_authorization_endpoint', client.use_mtls_endpoint_aliases, options?.[allowInsecureRequests] !== true);
|
|
1809
2084
|
const body = new URLSearchParams(parameters);
|
|
1810
2085
|
body.set('client_id', client.client_id);
|
|
1811
2086
|
const headers = prepareHeaders(options?.headers);
|
|
1812
2087
|
headers.set('accept', 'application/json');
|
|
1813
|
-
return authenticatedRequest(as, client,
|
|
2088
|
+
return authenticatedRequest(as, client, clientAuthentication, url, body, headers, options);
|
|
1814
2089
|
}
|
|
1815
2090
|
export async function processDeviceAuthorizationResponse(as, client, response) {
|
|
1816
2091
|
assertAs(as);
|
|
1817
2092
|
assertClient(client);
|
|
1818
2093
|
if (!looseInstanceOf(response, Response)) {
|
|
1819
|
-
throw
|
|
2094
|
+
throw CodedTypeError('"response" must be an instance of Response', ERR_INVALID_ARG_TYPE);
|
|
2095
|
+
}
|
|
2096
|
+
let challenges;
|
|
2097
|
+
if ((challenges = parseWwwAuthenticateChallenges(response))) {
|
|
2098
|
+
throw new WWWAuthenticateChallengeError('server responded with a challenge in the WWW-Authenticate HTTP Header', { cause: challenges, response });
|
|
1820
2099
|
}
|
|
1821
2100
|
if (response.status !== 200) {
|
|
1822
2101
|
let err;
|
|
1823
2102
|
if ((err = await handleOAuthBodyError(response))) {
|
|
1824
|
-
|
|
2103
|
+
await response.body?.cancel();
|
|
2104
|
+
throw new ResponseBodyError('server responded with an error in the response body', {
|
|
2105
|
+
cause: err,
|
|
2106
|
+
response,
|
|
2107
|
+
});
|
|
1825
2108
|
}
|
|
1826
|
-
throw
|
|
2109
|
+
throw OPE('"response" is not a conform Device Authorization Endpoint response', RESPONSE_IS_NOT_CONFORM, response);
|
|
1827
2110
|
}
|
|
1828
2111
|
assertReadableResponse(response);
|
|
2112
|
+
assertApplicationJson(response);
|
|
1829
2113
|
let json;
|
|
1830
2114
|
try {
|
|
1831
2115
|
json = await response.json();
|
|
1832
2116
|
}
|
|
1833
2117
|
catch (cause) {
|
|
1834
|
-
throw
|
|
2118
|
+
throw OPE('failed to parse "response" body as JSON', PARSE_ERROR, cause);
|
|
1835
2119
|
}
|
|
1836
2120
|
if (!isJsonObject(json)) {
|
|
1837
|
-
throw
|
|
1838
|
-
}
|
|
1839
|
-
if (!validateString(json.device_code)) {
|
|
1840
|
-
throw new OPE('"response" body "device_code" property must be a non-empty string');
|
|
1841
|
-
}
|
|
1842
|
-
if (!validateString(json.user_code)) {
|
|
1843
|
-
throw new OPE('"response" body "user_code" property must be a non-empty string');
|
|
2121
|
+
throw OPE('"response" body must be a top level object', INVALID_RESPONSE, { body: json });
|
|
1844
2122
|
}
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
}
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
}
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
2123
|
+
assertString(json.device_code, '"response" body "device_code" property', INVALID_RESPONSE, {
|
|
2124
|
+
body: json,
|
|
2125
|
+
});
|
|
2126
|
+
assertString(json.user_code, '"response" body "user_code" property', INVALID_RESPONSE, {
|
|
2127
|
+
body: json,
|
|
2128
|
+
});
|
|
2129
|
+
assertString(json.verification_uri, '"response" body "verification_uri" property', INVALID_RESPONSE, { body: json });
|
|
2130
|
+
let expiresIn = typeof json.expires_in !== 'number' ? parseFloat(json.expires_in) : json.expires_in;
|
|
2131
|
+
assertNumber(expiresIn, false, '"response" body "expires_in" property', INVALID_RESPONSE, {
|
|
2132
|
+
body: json,
|
|
2133
|
+
});
|
|
2134
|
+
json.expires_in = expiresIn;
|
|
2135
|
+
if (json.verification_uri_complete !== undefined) {
|
|
2136
|
+
assertString(json.verification_uri_complete, '"response" body "verification_uri_complete" property', INVALID_RESPONSE, { body: json });
|
|
1854
2137
|
}
|
|
1855
|
-
if (json.interval !== undefined
|
|
1856
|
-
|
|
2138
|
+
if (json.interval !== undefined) {
|
|
2139
|
+
assertNumber(json.interval, false, '"response" body "interval" property', INVALID_RESPONSE, {
|
|
2140
|
+
body: json,
|
|
2141
|
+
});
|
|
1857
2142
|
}
|
|
1858
2143
|
return json;
|
|
1859
2144
|
}
|
|
1860
|
-
export async function deviceCodeGrantRequest(as, client, deviceCode, options) {
|
|
2145
|
+
export async function deviceCodeGrantRequest(as, client, clientAuthentication, deviceCode, options) {
|
|
1861
2146
|
assertAs(as);
|
|
1862
2147
|
assertClient(client);
|
|
1863
|
-
|
|
1864
|
-
throw new TypeError('"deviceCode" must be a non-empty string');
|
|
1865
|
-
}
|
|
2148
|
+
assertString(deviceCode, '"deviceCode"');
|
|
1866
2149
|
const parameters = new URLSearchParams(options?.additionalParameters);
|
|
1867
2150
|
parameters.set('device_code', deviceCode);
|
|
1868
|
-
return tokenEndpointRequest(as, client, 'urn:ietf:params:oauth:grant-type:device_code', parameters, options);
|
|
2151
|
+
return tokenEndpointRequest(as, client, clientAuthentication, 'urn:ietf:params:oauth:grant-type:device_code', parameters, options);
|
|
1869
2152
|
}
|
|
1870
|
-
export async function processDeviceCodeResponse(as, client, response) {
|
|
1871
|
-
return processGenericAccessTokenResponse(as, client, response);
|
|
2153
|
+
export async function processDeviceCodeResponse(as, client, response, options) {
|
|
2154
|
+
return processGenericAccessTokenResponse(as, client, response, undefined, options);
|
|
1872
2155
|
}
|
|
1873
2156
|
export async function generateKeyPair(alg, options) {
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
}
|
|
1877
|
-
const algorithm = algToSubtle(alg, alg === 'EdDSA' ? (options?.crv ?? 'Ed25519') : undefined);
|
|
2157
|
+
assertString(alg, '"alg"');
|
|
2158
|
+
const algorithm = algToSubtle(alg);
|
|
1878
2159
|
if (alg.startsWith('PS') || alg.startsWith('RS')) {
|
|
1879
2160
|
Object.assign(algorithm, {
|
|
1880
2161
|
modulusLength: options?.modulusLength ?? 2048,
|
|
@@ -1892,46 +2173,53 @@ function normalizeHtu(htu) {
|
|
|
1892
2173
|
url.hash = '';
|
|
1893
2174
|
return url.href;
|
|
1894
2175
|
}
|
|
1895
|
-
async function validateDPoP(
|
|
1896
|
-
const
|
|
1897
|
-
if (
|
|
1898
|
-
throw
|
|
2176
|
+
async function validateDPoP(request, accessToken, accessTokenClaims, options) {
|
|
2177
|
+
const headerValue = request.headers.get('dpop');
|
|
2178
|
+
if (headerValue === null) {
|
|
2179
|
+
throw OPE('operation indicated DPoP use but the request has no DPoP HTTP Header', INVALID_REQUEST, { headers: request.headers });
|
|
1899
2180
|
}
|
|
1900
2181
|
if (request.headers.get('authorization')?.toLowerCase().startsWith('dpop ') === false) {
|
|
1901
|
-
throw
|
|
2182
|
+
throw OPE(`operation indicated DPoP use but the request's Authorization HTTP Header scheme is not DPoP`, INVALID_REQUEST, { headers: request.headers });
|
|
1902
2183
|
}
|
|
1903
2184
|
if (typeof accessTokenClaims.cnf?.jkt !== 'string') {
|
|
1904
|
-
throw
|
|
2185
|
+
throw OPE('operation indicated DPoP use but the JWT Access Token has no jkt confirmation claim', INVALID_REQUEST, { claims: accessTokenClaims });
|
|
1905
2186
|
}
|
|
1906
2187
|
const clockSkew = getClockSkew(options);
|
|
1907
|
-
const proof = await validateJwt(
|
|
1908
|
-
if (!jwk) {
|
|
1909
|
-
throw new OPE('DPoP Proof is missing the jwk header parameter');
|
|
1910
|
-
}
|
|
1911
|
-
const key = await importJwk(alg, jwk);
|
|
1912
|
-
if (key.type !== 'public') {
|
|
1913
|
-
throw new OPE('DPoP Proof jwk header parameter must contain a public key');
|
|
1914
|
-
}
|
|
1915
|
-
return key;
|
|
1916
|
-
}, clockSkew, getClockTolerance(options), undefined)
|
|
2188
|
+
const proof = await validateJwt(headerValue, checkSigningAlgorithm.bind(undefined, options?.signingAlgorithms, undefined, SUPPORTED_JWS_ALGS), clockSkew, getClockTolerance(options), undefined)
|
|
1917
2189
|
.then(checkJwtType.bind(undefined, 'dpop+jwt'))
|
|
1918
2190
|
.then(validatePresence.bind(undefined, ['iat', 'jti', 'ath', 'htm', 'htu']));
|
|
1919
2191
|
const now = epochTime() + clockSkew;
|
|
1920
2192
|
const diff = Math.abs(now - proof.claims.iat);
|
|
1921
2193
|
if (diff > 300) {
|
|
1922
|
-
throw
|
|
2194
|
+
throw OPE('DPoP Proof iat is not recent enough', JWT_TIMESTAMP_CHECK, {
|
|
2195
|
+
now,
|
|
2196
|
+
claims: proof.claims,
|
|
2197
|
+
claim: 'iat',
|
|
2198
|
+
});
|
|
1923
2199
|
}
|
|
1924
2200
|
if (proof.claims.htm !== request.method) {
|
|
1925
|
-
throw
|
|
2201
|
+
throw OPE('DPoP Proof htm mismatch', JWT_CLAIM_COMPARISON, {
|
|
2202
|
+
expected: request.method,
|
|
2203
|
+
claims: proof.claims,
|
|
2204
|
+
claim: 'htm',
|
|
2205
|
+
});
|
|
1926
2206
|
}
|
|
1927
2207
|
if (typeof proof.claims.htu !== 'string' ||
|
|
1928
2208
|
normalizeHtu(proof.claims.htu) !== normalizeHtu(request.url)) {
|
|
1929
|
-
throw
|
|
2209
|
+
throw OPE('DPoP Proof htu mismatch', JWT_CLAIM_COMPARISON, {
|
|
2210
|
+
expected: normalizeHtu(request.url),
|
|
2211
|
+
claims: proof.claims,
|
|
2212
|
+
claim: 'htu',
|
|
2213
|
+
});
|
|
1930
2214
|
}
|
|
1931
2215
|
{
|
|
1932
|
-
const expected = b64u(await crypto.subtle.digest('SHA-256',
|
|
2216
|
+
const expected = b64u(await crypto.subtle.digest('SHA-256', buf(accessToken)));
|
|
1933
2217
|
if (proof.claims.ath !== expected) {
|
|
1934
|
-
throw
|
|
2218
|
+
throw OPE('DPoP Proof ath mismatch', JWT_CLAIM_COMPARISON, {
|
|
2219
|
+
expected,
|
|
2220
|
+
claims: proof.claims,
|
|
2221
|
+
claim: 'ath',
|
|
2222
|
+
});
|
|
1935
2223
|
}
|
|
1936
2224
|
}
|
|
1937
2225
|
{
|
|
@@ -1960,25 +2248,44 @@ async function validateDPoP(as, request, accessToken, accessTokenClaims, options
|
|
|
1960
2248
|
};
|
|
1961
2249
|
break;
|
|
1962
2250
|
default:
|
|
1963
|
-
throw new UnsupportedOperationError();
|
|
2251
|
+
throw new UnsupportedOperationError('unsupported JWK key type', { cause: proof.header.jwk });
|
|
1964
2252
|
}
|
|
1965
|
-
const expected = b64u(await crypto.subtle.digest('SHA-256',
|
|
2253
|
+
const expected = b64u(await crypto.subtle.digest('SHA-256', buf(JSON.stringify(components))));
|
|
1966
2254
|
if (accessTokenClaims.cnf.jkt !== expected) {
|
|
1967
|
-
throw
|
|
2255
|
+
throw OPE('JWT Access Token confirmation mismatch', JWT_CLAIM_COMPARISON, {
|
|
2256
|
+
expected,
|
|
2257
|
+
claims: accessTokenClaims,
|
|
2258
|
+
claim: 'cnf.jkt',
|
|
2259
|
+
});
|
|
1968
2260
|
}
|
|
1969
2261
|
}
|
|
2262
|
+
const { 0: protectedHeader, 1: payload, 2: encodedSignature } = headerValue.split('.');
|
|
2263
|
+
const signature = b64u(encodedSignature);
|
|
2264
|
+
const { jwk, alg } = proof.header;
|
|
2265
|
+
if (!jwk) {
|
|
2266
|
+
throw OPE('DPoP Proof is missing the jwk header parameter', INVALID_REQUEST, {
|
|
2267
|
+
header: proof.header,
|
|
2268
|
+
});
|
|
2269
|
+
}
|
|
2270
|
+
const key = await importJwk(alg, jwk);
|
|
2271
|
+
if (key.type !== 'public') {
|
|
2272
|
+
throw OPE('DPoP Proof jwk header parameter must contain a public key', INVALID_REQUEST, {
|
|
2273
|
+
header: proof.header,
|
|
2274
|
+
});
|
|
2275
|
+
}
|
|
2276
|
+
await validateJwsSignature(protectedHeader, payload, key, signature);
|
|
1970
2277
|
}
|
|
1971
2278
|
export async function validateJwtAccessToken(as, request, expectedAudience, options) {
|
|
1972
2279
|
assertAs(as);
|
|
1973
2280
|
if (!looseInstanceOf(request, Request)) {
|
|
1974
|
-
throw
|
|
1975
|
-
}
|
|
1976
|
-
if (!validateString(expectedAudience)) {
|
|
1977
|
-
throw new OPE('"expectedAudience" must be a non-empty string');
|
|
2281
|
+
throw CodedTypeError('"request" must be an instance of Request', ERR_INVALID_ARG_TYPE);
|
|
1978
2282
|
}
|
|
2283
|
+
assertString(expectedAudience, '"expectedAudience"');
|
|
1979
2284
|
const authorization = request.headers.get('authorization');
|
|
1980
2285
|
if (authorization === null) {
|
|
1981
|
-
throw
|
|
2286
|
+
throw OPE('"request" is missing an Authorization HTTP Header', INVALID_REQUEST, {
|
|
2287
|
+
headers: request.headers,
|
|
2288
|
+
});
|
|
1982
2289
|
}
|
|
1983
2290
|
let { 0: scheme, 1: accessToken, length } = authorization.split(' ');
|
|
1984
2291
|
scheme = scheme.toLowerCase();
|
|
@@ -1987,10 +2294,14 @@ export async function validateJwtAccessToken(as, request, expectedAudience, opti
|
|
|
1987
2294
|
case 'bearer':
|
|
1988
2295
|
break;
|
|
1989
2296
|
default:
|
|
1990
|
-
throw new UnsupportedOperationError('unsupported Authorization HTTP Header scheme'
|
|
2297
|
+
throw new UnsupportedOperationError('unsupported Authorization HTTP Header scheme', {
|
|
2298
|
+
cause: { headers: request.headers },
|
|
2299
|
+
});
|
|
1991
2300
|
}
|
|
1992
2301
|
if (length !== 2) {
|
|
1993
|
-
throw
|
|
2302
|
+
throw OPE('invalid Authorization HTTP Header format', INVALID_REQUEST, {
|
|
2303
|
+
headers: request.headers,
|
|
2304
|
+
});
|
|
1994
2305
|
}
|
|
1995
2306
|
const requiredClaims = [
|
|
1996
2307
|
'iss',
|
|
@@ -2004,43 +2315,54 @@ export async function validateJwtAccessToken(as, request, expectedAudience, opti
|
|
|
2004
2315
|
if (options?.requireDPoP || scheme === 'dpop' || request.headers.has('dpop')) {
|
|
2005
2316
|
requiredClaims.push('cnf');
|
|
2006
2317
|
}
|
|
2007
|
-
const { claims } = await validateJwt(accessToken, checkSigningAlgorithm.bind(undefined,
|
|
2318
|
+
const { claims, header } = await validateJwt(accessToken, checkSigningAlgorithm.bind(undefined, options?.signingAlgorithms, undefined, SUPPORTED_JWS_ALGS), getClockSkew(options), getClockTolerance(options), undefined)
|
|
2008
2319
|
.then(checkJwtType.bind(undefined, 'at+jwt'))
|
|
2009
2320
|
.then(validatePresence.bind(undefined, requiredClaims))
|
|
2010
|
-
.then(validateIssuer.bind(undefined, as
|
|
2011
|
-
.then(validateAudience.bind(undefined, expectedAudience))
|
|
2321
|
+
.then(validateIssuer.bind(undefined, as))
|
|
2322
|
+
.then(validateAudience.bind(undefined, expectedAudience))
|
|
2323
|
+
.catch(reassignRSCode);
|
|
2012
2324
|
for (const claim of ['client_id', 'jti', 'sub']) {
|
|
2013
2325
|
if (typeof claims[claim] !== 'string') {
|
|
2014
|
-
throw
|
|
2326
|
+
throw OPE(`unexpected JWT "${claim}" claim type`, INVALID_REQUEST, { claims });
|
|
2015
2327
|
}
|
|
2016
2328
|
}
|
|
2017
2329
|
if ('cnf' in claims) {
|
|
2018
2330
|
if (!isJsonObject(claims.cnf)) {
|
|
2019
|
-
throw
|
|
2331
|
+
throw OPE('unexpected JWT "cnf" (confirmation) claim value', INVALID_REQUEST, { claims });
|
|
2020
2332
|
}
|
|
2021
2333
|
const { 0: cnf, length } = Object.keys(claims.cnf);
|
|
2022
2334
|
if (length) {
|
|
2023
2335
|
if (length !== 1) {
|
|
2024
|
-
throw new UnsupportedOperationError('multiple confirmation claims are not supported'
|
|
2336
|
+
throw new UnsupportedOperationError('multiple confirmation claims are not supported', {
|
|
2337
|
+
cause: { claims },
|
|
2338
|
+
});
|
|
2025
2339
|
}
|
|
2026
2340
|
if (cnf !== 'jkt') {
|
|
2027
|
-
throw new UnsupportedOperationError('unsupported JWT Confirmation method'
|
|
2341
|
+
throw new UnsupportedOperationError('unsupported JWT Confirmation method', {
|
|
2342
|
+
cause: { claims },
|
|
2343
|
+
});
|
|
2028
2344
|
}
|
|
2029
2345
|
}
|
|
2030
2346
|
}
|
|
2347
|
+
const { 0: protectedHeader, 1: payload, 2: encodedSignature } = accessToken.split('.');
|
|
2348
|
+
const signature = b64u(encodedSignature);
|
|
2349
|
+
const key = await getPublicSigKeyFromIssuerJwksUri(as, options, header);
|
|
2350
|
+
await validateJwsSignature(protectedHeader, payload, key, signature);
|
|
2031
2351
|
if (options?.requireDPoP ||
|
|
2032
2352
|
scheme === 'dpop' ||
|
|
2033
2353
|
claims.cnf?.jkt !== undefined ||
|
|
2034
2354
|
request.headers.has('dpop')) {
|
|
2035
|
-
await validateDPoP(
|
|
2355
|
+
await validateDPoP(request, accessToken, claims, options).catch(reassignRSCode);
|
|
2036
2356
|
}
|
|
2037
2357
|
return claims;
|
|
2038
2358
|
}
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
export const
|
|
2046
|
-
export const
|
|
2359
|
+
function reassignRSCode(err) {
|
|
2360
|
+
if (err instanceof OperationProcessingError && err?.code === INVALID_REQUEST) {
|
|
2361
|
+
err.code = INVALID_RESPONSE;
|
|
2362
|
+
}
|
|
2363
|
+
throw err;
|
|
2364
|
+
}
|
|
2365
|
+
export const _nopkce = Symbol();
|
|
2366
|
+
export const _nodiscoverycheck = Symbol();
|
|
2367
|
+
export const _expectedIssuer = Symbol();
|
|
2368
|
+
//# sourceMappingURL=index.js.map
|