oauth4webapi 2.17.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -9
- package/build/index.d.ts +820 -548
- package/build/index.js +1076 -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.0';
|
|
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 private 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,86 @@ 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
|
+
assertNumber(json.expires_in, false, '"response" body "expires_in" property', INVALID_RESPONSE, {
|
|
821
|
+
body: json,
|
|
822
|
+
});
|
|
707
823
|
return json;
|
|
708
824
|
}
|
|
709
|
-
|
|
710
|
-
if (!
|
|
711
|
-
throw
|
|
825
|
+
function assertDPoP(option) {
|
|
826
|
+
if (!branded.has(option)) {
|
|
827
|
+
throw CodedTypeError('"options.DPoP" is not a valid DPoPHandle', ERR_INVALID_ARG_VALUE);
|
|
712
828
|
}
|
|
829
|
+
}
|
|
830
|
+
async function resourceRequest(accessToken, method, url, headers, body, options) {
|
|
831
|
+
assertString(accessToken, '"accessToken"');
|
|
713
832
|
if (!(url instanceof URL)) {
|
|
714
|
-
throw
|
|
833
|
+
throw CodedTypeError('"url" must be an instance of URL', ERR_INVALID_ARG_TYPE);
|
|
715
834
|
}
|
|
835
|
+
checkProtocol(url, options?.[allowInsecureRequests] !== true);
|
|
716
836
|
headers = prepareHeaders(headers);
|
|
717
|
-
if (options?.DPoP
|
|
718
|
-
|
|
837
|
+
if (options?.DPoP) {
|
|
838
|
+
assertDPoP(options.DPoP);
|
|
839
|
+
await options.DPoP.addProof(url, headers, method.toUpperCase(), accessToken);
|
|
840
|
+
headers.set('authorization', `DPoP ${accessToken}`);
|
|
719
841
|
}
|
|
720
842
|
else {
|
|
721
|
-
|
|
722
|
-
headers.set('authorization', `DPoP ${accessToken}`);
|
|
843
|
+
headers.set('authorization', `Bearer ${accessToken}`);
|
|
723
844
|
}
|
|
724
|
-
|
|
845
|
+
const response = await (options?.[customFetch] || fetch)(url.href, {
|
|
725
846
|
body,
|
|
726
847
|
headers: Object.fromEntries(headers.entries()),
|
|
727
848
|
method,
|
|
728
849
|
redirect: 'manual',
|
|
729
|
-
signal: options?.signal ? signal(options.signal) :
|
|
730
|
-
})
|
|
850
|
+
signal: options?.signal ? signal(options.signal) : undefined,
|
|
851
|
+
});
|
|
852
|
+
options?.DPoP?.cacheNonce(response);
|
|
853
|
+
return response;
|
|
854
|
+
}
|
|
855
|
+
export async function protectedResourceRequest(accessToken, method, url, headers, body, options) {
|
|
856
|
+
return resourceRequest(accessToken, method, url, headers, body, options).then((response) => {
|
|
857
|
+
let challenges;
|
|
858
|
+
if ((challenges = parseWwwAuthenticateChallenges(response))) {
|
|
859
|
+
throw new WWWAuthenticateChallengeError('server responded with a challenge in the WWW-Authenticate HTTP Header', { cause: challenges, response });
|
|
860
|
+
}
|
|
861
|
+
return response;
|
|
862
|
+
});
|
|
731
863
|
}
|
|
732
864
|
export async function userInfoRequest(as, client, accessToken, options) {
|
|
733
865
|
assertAs(as);
|
|
734
866
|
assertClient(client);
|
|
735
|
-
const url = resolveEndpoint(as, 'userinfo_endpoint',
|
|
867
|
+
const url = resolveEndpoint(as, 'userinfo_endpoint', client.use_mtls_endpoint_aliases, options?.[allowInsecureRequests] !== true);
|
|
736
868
|
const headers = prepareHeaders(options?.headers);
|
|
737
869
|
if (client.userinfo_signed_response_alg) {
|
|
738
870
|
headers.set('accept', 'application/jwt');
|
|
@@ -741,14 +873,14 @@ export async function userInfoRequest(as, client, accessToken, options) {
|
|
|
741
873
|
headers.set('accept', 'application/json');
|
|
742
874
|
headers.append('accept', 'application/jwt');
|
|
743
875
|
}
|
|
744
|
-
return
|
|
876
|
+
return resourceRequest(accessToken, 'GET', url, headers, null, {
|
|
745
877
|
...options,
|
|
746
878
|
[clockSkew]: getClockSkew(client),
|
|
747
879
|
});
|
|
748
880
|
}
|
|
749
881
|
let jwksMap;
|
|
750
882
|
function setJwksCache(as, jwks, uat, cache) {
|
|
751
|
-
jwksMap
|
|
883
|
+
jwksMap ||= new WeakMap();
|
|
752
884
|
jwksMap.set(as, {
|
|
753
885
|
jwks,
|
|
754
886
|
uat,
|
|
@@ -782,7 +914,7 @@ function clearJwksCache(as, cache) {
|
|
|
782
914
|
}
|
|
783
915
|
async function getPublicSigKeyFromIssuerJwksUri(as, options, header) {
|
|
784
916
|
const { alg, kid } = header;
|
|
785
|
-
checkSupportedJwsAlg(
|
|
917
|
+
checkSupportedJwsAlg(header);
|
|
786
918
|
if (!jwksMap?.has(as) && isFreshJwksCache(options?.[jwksCache])) {
|
|
787
919
|
setJwksCache(as, options?.[jwksCache].jwks, options?.[jwksCache].uat);
|
|
788
920
|
}
|
|
@@ -814,7 +946,7 @@ async function getPublicSigKeyFromIssuerJwksUri(as, options, header) {
|
|
|
814
946
|
kty = 'OKP';
|
|
815
947
|
break;
|
|
816
948
|
default:
|
|
817
|
-
throw new UnsupportedOperationError();
|
|
949
|
+
throw new UnsupportedOperationError('unsupported JWS algorithm', { cause: { alg } });
|
|
818
950
|
}
|
|
819
951
|
const candidates = jwks.keys.filter((jwk) => {
|
|
820
952
|
if (jwk.kty !== kty) {
|
|
@@ -836,7 +968,8 @@ async function getPublicSigKeyFromIssuerJwksUri(as, options, header) {
|
|
|
836
968
|
case alg === 'ES256' && jwk.crv !== 'P-256':
|
|
837
969
|
case alg === 'ES384' && jwk.crv !== 'P-384':
|
|
838
970
|
case alg === 'ES512' && jwk.crv !== 'P-521':
|
|
839
|
-
case alg === '
|
|
971
|
+
case alg === 'Ed25519' && jwk.crv !== 'Ed25519':
|
|
972
|
+
case alg === 'EdDSA' && jwk.crv !== 'Ed25519':
|
|
840
973
|
return false;
|
|
841
974
|
}
|
|
842
975
|
return true;
|
|
@@ -847,221 +980,219 @@ async function getPublicSigKeyFromIssuerJwksUri(as, options, header) {
|
|
|
847
980
|
clearJwksCache(as, options?.[jwksCache]);
|
|
848
981
|
return getPublicSigKeyFromIssuerJwksUri(as, options, header);
|
|
849
982
|
}
|
|
850
|
-
throw
|
|
983
|
+
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
984
|
}
|
|
852
985
|
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');
|
|
986
|
+
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
987
|
}
|
|
859
|
-
return
|
|
988
|
+
return importJwk(alg, jwk);
|
|
860
989
|
}
|
|
861
990
|
export const skipSubjectCheck = Symbol();
|
|
862
991
|
function getContentType(response) {
|
|
863
992
|
return response.headers.get('content-type')?.split(';')[0];
|
|
864
993
|
}
|
|
865
|
-
export async function processUserInfoResponse(as, client, expectedSubject, response) {
|
|
994
|
+
export async function processUserInfoResponse(as, client, expectedSubject, response, options) {
|
|
866
995
|
assertAs(as);
|
|
867
996
|
assertClient(client);
|
|
868
997
|
if (!looseInstanceOf(response, Response)) {
|
|
869
|
-
throw
|
|
998
|
+
throw CodedTypeError('"response" must be an instance of Response', ERR_INVALID_ARG_TYPE);
|
|
999
|
+
}
|
|
1000
|
+
let challenges;
|
|
1001
|
+
if ((challenges = parseWwwAuthenticateChallenges(response))) {
|
|
1002
|
+
throw new WWWAuthenticateChallengeError('server responded with a challenge in the WWW-Authenticate HTTP Header', { cause: challenges, response });
|
|
870
1003
|
}
|
|
871
1004
|
if (response.status !== 200) {
|
|
872
|
-
throw
|
|
1005
|
+
throw OPE('"response" is not a conform UserInfo Endpoint response', RESPONSE_IS_NOT_CONFORM, response);
|
|
873
1006
|
}
|
|
1007
|
+
assertReadableResponse(response);
|
|
874
1008
|
let json;
|
|
875
1009
|
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])
|
|
1010
|
+
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
1011
|
.then(validateOptionalAudience.bind(undefined, client.client_id))
|
|
879
|
-
.then(validateOptionalIssuer.bind(undefined, as
|
|
880
|
-
|
|
1012
|
+
.then(validateOptionalIssuer.bind(undefined, as));
|
|
1013
|
+
jwtRefs.set(response, jwt);
|
|
881
1014
|
json = claims;
|
|
882
1015
|
}
|
|
883
1016
|
else {
|
|
884
1017
|
if (client.userinfo_signed_response_alg) {
|
|
885
|
-
throw
|
|
1018
|
+
throw OPE('JWT UserInfo Response expected', JWT_USERINFO_EXPECTED, response);
|
|
886
1019
|
}
|
|
887
|
-
|
|
1020
|
+
assertApplicationJson(response);
|
|
888
1021
|
try {
|
|
889
1022
|
json = await response.json();
|
|
890
1023
|
}
|
|
891
1024
|
catch (cause) {
|
|
892
|
-
throw
|
|
1025
|
+
throw OPE('failed to parse "response" body as JSON', PARSE_ERROR, cause);
|
|
893
1026
|
}
|
|
894
1027
|
}
|
|
895
1028
|
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');
|
|
1029
|
+
throw OPE('"response" body must be a top level object', INVALID_RESPONSE, { body: json });
|
|
900
1030
|
}
|
|
1031
|
+
assertString(json.sub, '"response" body "sub" property', INVALID_RESPONSE, { body: json });
|
|
901
1032
|
switch (expectedSubject) {
|
|
902
1033
|
case skipSubjectCheck:
|
|
903
1034
|
break;
|
|
904
1035
|
default:
|
|
905
|
-
|
|
906
|
-
throw new OPE('"expectedSubject" must be a non-empty string');
|
|
907
|
-
}
|
|
1036
|
+
assertString(expectedSubject, '"expectedSubject"');
|
|
908
1037
|
if (json.sub !== expectedSubject) {
|
|
909
|
-
throw
|
|
1038
|
+
throw OPE('unexpected "response" body "sub" property value', JSON_ATTRIBUTE_COMPARISON, {
|
|
1039
|
+
expected: expectedSubject,
|
|
1040
|
+
body: json,
|
|
1041
|
+
attribute: 'sub',
|
|
1042
|
+
});
|
|
910
1043
|
}
|
|
911
1044
|
}
|
|
912
1045
|
return json;
|
|
913
1046
|
}
|
|
914
|
-
async function authenticatedRequest(as, client,
|
|
915
|
-
await clientAuthentication(as, client, body, headers
|
|
1047
|
+
async function authenticatedRequest(as, client, clientAuthentication, url, body, headers, options) {
|
|
1048
|
+
await clientAuthentication(as, client, body, headers);
|
|
916
1049
|
headers.set('content-type', 'application/x-www-form-urlencoded;charset=UTF-8');
|
|
917
1050
|
return (options?.[customFetch] || fetch)(url.href, {
|
|
918
1051
|
body,
|
|
919
1052
|
headers: Object.fromEntries(headers.entries()),
|
|
920
|
-
method,
|
|
1053
|
+
method: 'POST',
|
|
921
1054
|
redirect: 'manual',
|
|
922
|
-
signal: options?.signal ? signal(options.signal) :
|
|
923
|
-
})
|
|
1055
|
+
signal: options?.signal ? signal(options.signal) : undefined,
|
|
1056
|
+
});
|
|
924
1057
|
}
|
|
925
|
-
async function tokenEndpointRequest(as, client, grantType, parameters, options) {
|
|
926
|
-
const url = resolveEndpoint(as, 'token_endpoint',
|
|
1058
|
+
async function tokenEndpointRequest(as, client, clientAuthentication, grantType, parameters, options) {
|
|
1059
|
+
const url = resolveEndpoint(as, 'token_endpoint', client.use_mtls_endpoint_aliases, options?.[allowInsecureRequests] !== true);
|
|
927
1060
|
parameters.set('grant_type', grantType);
|
|
928
1061
|
const headers = prepareHeaders(options?.headers);
|
|
929
1062
|
headers.set('accept', 'application/json');
|
|
930
1063
|
if (options?.DPoP !== undefined) {
|
|
931
|
-
|
|
1064
|
+
assertDPoP(options.DPoP);
|
|
1065
|
+
await options.DPoP.addProof(url, headers, 'POST');
|
|
932
1066
|
}
|
|
933
|
-
|
|
1067
|
+
const response = await authenticatedRequest(as, client, clientAuthentication, url, parameters, headers, options);
|
|
1068
|
+
options?.DPoP?.cacheNonce(response);
|
|
1069
|
+
return response;
|
|
934
1070
|
}
|
|
935
|
-
export async function refreshTokenGrantRequest(as, client, refreshToken, options) {
|
|
1071
|
+
export async function refreshTokenGrantRequest(as, client, clientAuthentication, refreshToken, options) {
|
|
936
1072
|
assertAs(as);
|
|
937
1073
|
assertClient(client);
|
|
938
|
-
|
|
939
|
-
throw new TypeError('"refreshToken" must be a non-empty string');
|
|
940
|
-
}
|
|
1074
|
+
assertString(refreshToken, '"refreshToken"');
|
|
941
1075
|
const parameters = new URLSearchParams(options?.additionalParameters);
|
|
942
1076
|
parameters.set('refresh_token', refreshToken);
|
|
943
|
-
return tokenEndpointRequest(as, client, 'refresh_token', parameters, options);
|
|
1077
|
+
return tokenEndpointRequest(as, client, clientAuthentication, 'refresh_token', parameters, options);
|
|
944
1078
|
}
|
|
945
1079
|
const idTokenClaims = new WeakMap();
|
|
946
|
-
const
|
|
1080
|
+
const jwtRefs = new WeakMap();
|
|
947
1081
|
export function getValidatedIdTokenClaims(ref) {
|
|
948
1082
|
if (!ref.id_token) {
|
|
949
1083
|
return undefined;
|
|
950
1084
|
}
|
|
951
1085
|
const claims = idTokenClaims.get(ref);
|
|
952
1086
|
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');
|
|
1087
|
+
throw CodedTypeError('"ref" was already garbage collected or did not resolve from the proper sources', ERR_INVALID_ARG_VALUE);
|
|
961
1088
|
}
|
|
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));
|
|
1089
|
+
return claims;
|
|
970
1090
|
}
|
|
971
|
-
async function
|
|
1091
|
+
export async function validateApplicationLevelSignature(as, ref, options) {
|
|
972
1092
|
assertAs(as);
|
|
973
|
-
if (!
|
|
974
|
-
throw
|
|
1093
|
+
if (!jwtRefs.has(ref)) {
|
|
1094
|
+
throw CodedTypeError('"ref" does not contain a processed JWT Response to verify the signature of', ERR_INVALID_ARG_VALUE);
|
|
975
1095
|
}
|
|
976
|
-
const { 0: protectedHeader, 1: payload, 2: encodedSignature
|
|
1096
|
+
const { 0: protectedHeader, 1: payload, 2: encodedSignature } = jwtRefs.get(ref).split('.');
|
|
977
1097
|
const header = JSON.parse(buf(b64u(protectedHeader)));
|
|
978
1098
|
if (header.alg.startsWith('HS')) {
|
|
979
|
-
throw new UnsupportedOperationError();
|
|
1099
|
+
throw new UnsupportedOperationError('unsupported JWS algorithm', { cause: { alg: header.alg } });
|
|
980
1100
|
}
|
|
981
1101
|
let key;
|
|
982
1102
|
key = await getPublicSigKeyFromIssuerJwksUri(as, options, header);
|
|
983
1103
|
await validateJwsSignature(protectedHeader, payload, key, b64u(encodedSignature));
|
|
984
1104
|
}
|
|
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) {
|
|
1105
|
+
async function processGenericAccessTokenResponse(as, client, response, additionalRequiredIdTokenClaims, options) {
|
|
992
1106
|
assertAs(as);
|
|
993
1107
|
assertClient(client);
|
|
994
1108
|
if (!looseInstanceOf(response, Response)) {
|
|
995
|
-
throw
|
|
1109
|
+
throw CodedTypeError('"response" must be an instance of Response', ERR_INVALID_ARG_TYPE);
|
|
1110
|
+
}
|
|
1111
|
+
let challenges;
|
|
1112
|
+
if ((challenges = parseWwwAuthenticateChallenges(response))) {
|
|
1113
|
+
throw new WWWAuthenticateChallengeError('server responded with a challenge in the WWW-Authenticate HTTP Header', { cause: challenges, response });
|
|
996
1114
|
}
|
|
997
1115
|
if (response.status !== 200) {
|
|
998
1116
|
let err;
|
|
999
1117
|
if ((err = await handleOAuthBodyError(response))) {
|
|
1000
|
-
|
|
1118
|
+
await response.body?.cancel();
|
|
1119
|
+
throw new ResponseBodyError('server responded with an error in the response body', {
|
|
1120
|
+
cause: err,
|
|
1121
|
+
response,
|
|
1122
|
+
});
|
|
1001
1123
|
}
|
|
1002
|
-
throw
|
|
1124
|
+
throw OPE('"response" is not a conform Token Endpoint response', RESPONSE_IS_NOT_CONFORM, response);
|
|
1003
1125
|
}
|
|
1004
1126
|
assertReadableResponse(response);
|
|
1127
|
+
assertApplicationJson(response);
|
|
1005
1128
|
let json;
|
|
1006
1129
|
try {
|
|
1007
1130
|
json = await response.json();
|
|
1008
1131
|
}
|
|
1009
1132
|
catch (cause) {
|
|
1010
|
-
throw
|
|
1133
|
+
throw OPE('failed to parse "response" body as JSON', PARSE_ERROR, cause);
|
|
1011
1134
|
}
|
|
1012
1135
|
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');
|
|
1136
|
+
throw OPE('"response" body must be a top level object', INVALID_RESPONSE, { body: json });
|
|
1020
1137
|
}
|
|
1138
|
+
assertString(json.access_token, '"response" body "access_token" property', INVALID_RESPONSE, {
|
|
1139
|
+
body: json,
|
|
1140
|
+
});
|
|
1141
|
+
assertString(json.token_type, '"response" body "token_type" property', INVALID_RESPONSE, {
|
|
1142
|
+
body: json,
|
|
1143
|
+
});
|
|
1021
1144
|
json.token_type = json.token_type.toLowerCase();
|
|
1022
1145
|
if (json.token_type !== 'dpop' && json.token_type !== 'bearer') {
|
|
1023
|
-
throw new UnsupportedOperationError('unsupported `token_type` value');
|
|
1146
|
+
throw new UnsupportedOperationError('unsupported `token_type` value', { cause: { body: json } });
|
|
1024
1147
|
}
|
|
1025
|
-
if (json.expires_in !== undefined
|
|
1026
|
-
(
|
|
1027
|
-
throw new OPE('"response" body "expires_in" property must be a positive number');
|
|
1148
|
+
if (json.expires_in !== undefined) {
|
|
1149
|
+
assertNumber(json.expires_in, false, '"response" body "expires_in" property', INVALID_RESPONSE, { body: json });
|
|
1028
1150
|
}
|
|
1029
|
-
if (
|
|
1030
|
-
json.refresh_token
|
|
1031
|
-
|
|
1032
|
-
|
|
1151
|
+
if (json.refresh_token !== undefined) {
|
|
1152
|
+
assertString(json.refresh_token, '"response" body "refresh_token" property', INVALID_RESPONSE, {
|
|
1153
|
+
body: json,
|
|
1154
|
+
});
|
|
1033
1155
|
}
|
|
1034
1156
|
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
|
-
|
|
1157
|
+
throw OPE('"response" body "scope" property must be a string', INVALID_RESPONSE, { body: json });
|
|
1158
|
+
}
|
|
1159
|
+
if (json.id_token !== undefined) {
|
|
1160
|
+
assertString(json.id_token, '"response" body "id_token" property', INVALID_RESPONSE, {
|
|
1161
|
+
body: json,
|
|
1162
|
+
});
|
|
1163
|
+
const requiredClaims = ['aud', 'exp', 'iat', 'iss', 'sub'];
|
|
1164
|
+
if (client.require_auth_time === true) {
|
|
1165
|
+
requiredClaims.push('auth_time');
|
|
1166
|
+
}
|
|
1167
|
+
if (client.default_max_age !== undefined) {
|
|
1168
|
+
assertNumber(client.default_max_age, false, '"client.default_max_age"');
|
|
1169
|
+
requiredClaims.push('auth_time');
|
|
1170
|
+
}
|
|
1171
|
+
if (additionalRequiredIdTokenClaims?.length) {
|
|
1172
|
+
requiredClaims.push(...additionalRequiredIdTokenClaims);
|
|
1173
|
+
}
|
|
1174
|
+
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])
|
|
1175
|
+
.then(validatePresence.bind(undefined, requiredClaims))
|
|
1176
|
+
.then(validateIssuer.bind(undefined, as))
|
|
1177
|
+
.then(validateAudience.bind(undefined, client.client_id));
|
|
1178
|
+
if (Array.isArray(claims.aud) && claims.aud.length !== 1) {
|
|
1179
|
+
if (claims.azp === undefined) {
|
|
1180
|
+
throw OPE('ID Token "aud" (audience) claim includes additional untrusted audiences', JWT_CLAIM_COMPARISON, { claims, claim: 'aud' });
|
|
1053
1181
|
}
|
|
1054
|
-
if (claims.
|
|
1055
|
-
(
|
|
1056
|
-
throw new OPE('ID Token "auth_time" (authentication time) must be a positive number');
|
|
1182
|
+
if (claims.azp !== client.client_id) {
|
|
1183
|
+
throw OPE('unexpected ID Token "azp" (authorized party) claim value', JWT_CLAIM_COMPARISON, { expected: client.client_id, claims, claim: 'azp' });
|
|
1057
1184
|
}
|
|
1058
|
-
idTokenClaims.set(json, [claims, jwt]);
|
|
1059
1185
|
}
|
|
1186
|
+
if (claims.auth_time !== undefined) {
|
|
1187
|
+
assertNumber(claims.auth_time, false, 'ID Token "auth_time" (authentication time)', INVALID_RESPONSE, { claims });
|
|
1188
|
+
}
|
|
1189
|
+
jwtRefs.set(response, jwt);
|
|
1190
|
+
idTokenClaims.set(json, claims);
|
|
1060
1191
|
}
|
|
1061
1192
|
return json;
|
|
1062
1193
|
}
|
|
1063
|
-
export async function processRefreshTokenResponse(as, client, response) {
|
|
1064
|
-
return processGenericAccessTokenResponse(as, client, response);
|
|
1194
|
+
export async function processRefreshTokenResponse(as, client, response, options) {
|
|
1195
|
+
return processGenericAccessTokenResponse(as, client, response, undefined, options);
|
|
1065
1196
|
}
|
|
1066
1197
|
function validateOptionalAudience(expected, result) {
|
|
1067
1198
|
if (result.claims.aud !== undefined) {
|
|
@@ -1072,23 +1203,36 @@ function validateOptionalAudience(expected, result) {
|
|
|
1072
1203
|
function validateAudience(expected, result) {
|
|
1073
1204
|
if (Array.isArray(result.claims.aud)) {
|
|
1074
1205
|
if (!result.claims.aud.includes(expected)) {
|
|
1075
|
-
throw
|
|
1206
|
+
throw OPE('unexpected JWT "aud" (audience) claim value', JWT_CLAIM_COMPARISON, {
|
|
1207
|
+
expected,
|
|
1208
|
+
claims: result.claims,
|
|
1209
|
+
claim: 'aud',
|
|
1210
|
+
});
|
|
1076
1211
|
}
|
|
1077
1212
|
}
|
|
1078
1213
|
else if (result.claims.aud !== expected) {
|
|
1079
|
-
throw
|
|
1214
|
+
throw OPE('unexpected JWT "aud" (audience) claim value', JWT_CLAIM_COMPARISON, {
|
|
1215
|
+
expected,
|
|
1216
|
+
claims: result.claims,
|
|
1217
|
+
claim: 'aud',
|
|
1218
|
+
});
|
|
1080
1219
|
}
|
|
1081
1220
|
return result;
|
|
1082
1221
|
}
|
|
1083
|
-
function validateOptionalIssuer(
|
|
1222
|
+
function validateOptionalIssuer(as, result) {
|
|
1084
1223
|
if (result.claims.iss !== undefined) {
|
|
1085
|
-
return validateIssuer(
|
|
1224
|
+
return validateIssuer(as, result);
|
|
1086
1225
|
}
|
|
1087
1226
|
return result;
|
|
1088
1227
|
}
|
|
1089
|
-
function validateIssuer(
|
|
1228
|
+
function validateIssuer(as, result) {
|
|
1229
|
+
const expected = as[_expectedIssuer]?.(result) ?? as.issuer;
|
|
1090
1230
|
if (result.claims.iss !== expected) {
|
|
1091
|
-
throw
|
|
1231
|
+
throw OPE('unexpected JWT "iss" (issuer) claim value', JWT_CLAIM_COMPARISON, {
|
|
1232
|
+
expected,
|
|
1233
|
+
claims: result.claims,
|
|
1234
|
+
claim: 'iss',
|
|
1235
|
+
});
|
|
1092
1236
|
}
|
|
1093
1237
|
return result;
|
|
1094
1238
|
}
|
|
@@ -1097,27 +1241,25 @@ function brand(searchParams) {
|
|
|
1097
1241
|
branded.add(searchParams);
|
|
1098
1242
|
return searchParams;
|
|
1099
1243
|
}
|
|
1100
|
-
export async function authorizationCodeGrantRequest(as, client, callbackParameters, redirectUri, codeVerifier, options) {
|
|
1244
|
+
export async function authorizationCodeGrantRequest(as, client, clientAuthentication, callbackParameters, redirectUri, codeVerifier, options) {
|
|
1101
1245
|
assertAs(as);
|
|
1102
1246
|
assertClient(client);
|
|
1103
1247
|
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');
|
|
1248
|
+
throw CodedTypeError('"callbackParameters" must be an instance of URLSearchParams obtained from "validateAuthResponse()", or "validateJwtAuthResponse()', ERR_INVALID_ARG_VALUE);
|
|
1111
1249
|
}
|
|
1250
|
+
assertString(redirectUri, '"redirectUri"');
|
|
1112
1251
|
const code = getURLSearchParameter(callbackParameters, 'code');
|
|
1113
1252
|
if (!code) {
|
|
1114
|
-
throw
|
|
1253
|
+
throw OPE('no authorization code in "callbackParameters"', INVALID_RESPONSE);
|
|
1115
1254
|
}
|
|
1116
1255
|
const parameters = new URLSearchParams(options?.additionalParameters);
|
|
1117
1256
|
parameters.set('redirect_uri', redirectUri);
|
|
1118
|
-
parameters.set('code_verifier', codeVerifier);
|
|
1119
1257
|
parameters.set('code', code);
|
|
1120
|
-
|
|
1258
|
+
if (codeVerifier !== _nopkce) {
|
|
1259
|
+
assertString(codeVerifier, '"codeVerifier"');
|
|
1260
|
+
parameters.set('code_verifier', codeVerifier);
|
|
1261
|
+
}
|
|
1262
|
+
return tokenEndpointRequest(as, client, clientAuthentication, 'authorization_code', parameters, options);
|
|
1121
1263
|
}
|
|
1122
1264
|
const jwtClaimNames = {
|
|
1123
1265
|
aud: 'audience',
|
|
@@ -1134,138 +1276,190 @@ const jwtClaimNames = {
|
|
|
1134
1276
|
htm: 'http method',
|
|
1135
1277
|
htu: 'http uri',
|
|
1136
1278
|
cnf: 'confirmation',
|
|
1279
|
+
auth_time: 'authentication time',
|
|
1137
1280
|
};
|
|
1138
1281
|
function validatePresence(required, result) {
|
|
1139
1282
|
for (const claim of required) {
|
|
1140
1283
|
if (result.claims[claim] === undefined) {
|
|
1141
|
-
throw
|
|
1284
|
+
throw OPE(`JWT "${claim}" (${jwtClaimNames[claim]}) claim missing`, INVALID_RESPONSE, {
|
|
1285
|
+
claims: result.claims,
|
|
1286
|
+
});
|
|
1142
1287
|
}
|
|
1143
1288
|
}
|
|
1144
1289
|
return result;
|
|
1145
1290
|
}
|
|
1146
1291
|
export const expectNoNonce = Symbol();
|
|
1147
1292
|
export const skipAuthTimeCheck = Symbol();
|
|
1148
|
-
export async function
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1293
|
+
export async function processAuthorizationCodeResponse(as, client, response, options) {
|
|
1294
|
+
if (typeof options?.expectedNonce === 'string' ||
|
|
1295
|
+
typeof options?.maxAge === 'number' ||
|
|
1296
|
+
options?.requireIdToken) {
|
|
1297
|
+
return processAuthorizationCodeOpenIDResponse(as, client, response, options.expectedNonce, options.maxAge, {
|
|
1298
|
+
[jweDecrypt]: options[jweDecrypt],
|
|
1299
|
+
});
|
|
1152
1300
|
}
|
|
1153
|
-
|
|
1154
|
-
|
|
1301
|
+
return processAuthorizationCodeOAuth2Response(as, client, response, options);
|
|
1302
|
+
}
|
|
1303
|
+
async function processAuthorizationCodeOpenIDResponse(as, client, response, expectedNonce, maxAge, options) {
|
|
1304
|
+
const additionalRequiredClaims = [];
|
|
1305
|
+
switch (expectedNonce) {
|
|
1306
|
+
case undefined:
|
|
1307
|
+
expectedNonce = expectNoNonce;
|
|
1308
|
+
break;
|
|
1309
|
+
case expectNoNonce:
|
|
1310
|
+
break;
|
|
1311
|
+
default:
|
|
1312
|
+
assertString(expectedNonce, '"expectedNonce" argument');
|
|
1313
|
+
additionalRequiredClaims.push('nonce');
|
|
1155
1314
|
}
|
|
1156
|
-
maxAge
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1315
|
+
maxAge ??= client.default_max_age;
|
|
1316
|
+
switch (maxAge) {
|
|
1317
|
+
case undefined:
|
|
1318
|
+
maxAge = skipAuthTimeCheck;
|
|
1319
|
+
break;
|
|
1320
|
+
case skipAuthTimeCheck:
|
|
1321
|
+
break;
|
|
1322
|
+
default:
|
|
1323
|
+
assertNumber(maxAge, false, '"maxAge" argument');
|
|
1324
|
+
additionalRequiredClaims.push('auth_time');
|
|
1161
1325
|
}
|
|
1326
|
+
const result = await processGenericAccessTokenResponse(as, client, response, additionalRequiredClaims, options);
|
|
1327
|
+
assertString(result.id_token, '"response" body "id_token" property', INVALID_RESPONSE, {
|
|
1328
|
+
body: result,
|
|
1329
|
+
});
|
|
1330
|
+
const claims = getValidatedIdTokenClaims(result);
|
|
1162
1331
|
if (maxAge !== skipAuthTimeCheck) {
|
|
1163
|
-
if (typeof maxAge !== 'number' || maxAge < 0) {
|
|
1164
|
-
throw new TypeError('"maxAge" must be a non-negative number');
|
|
1165
|
-
}
|
|
1166
1332
|
const now = epochTime() + getClockSkew(client);
|
|
1167
1333
|
const tolerance = getClockTolerance(client);
|
|
1168
1334
|
if (claims.auth_time + maxAge < now - tolerance) {
|
|
1169
|
-
throw
|
|
1335
|
+
throw OPE('too much time has elapsed since the last End-User authentication', JWT_TIMESTAMP_CHECK, { claims, now, tolerance, claim: 'auth_time' });
|
|
1170
1336
|
}
|
|
1171
1337
|
}
|
|
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
|
-
}
|
|
1338
|
+
if (expectedNonce === expectNoNonce) {
|
|
1339
|
+
if (claims.nonce !== undefined) {
|
|
1340
|
+
throw OPE('unexpected ID Token "nonce" claim value', JWT_CLAIM_COMPARISON, {
|
|
1341
|
+
expected: undefined,
|
|
1342
|
+
claims,
|
|
1343
|
+
claim: 'nonce',
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
else if (claims.nonce !== expectedNonce) {
|
|
1348
|
+
throw OPE('unexpected ID Token "nonce" claim value', JWT_CLAIM_COMPARISON, {
|
|
1349
|
+
expected: expectedNonce,
|
|
1350
|
+
claims,
|
|
1351
|
+
claim: 'nonce',
|
|
1352
|
+
});
|
|
1189
1353
|
}
|
|
1190
1354
|
return result;
|
|
1191
1355
|
}
|
|
1192
|
-
|
|
1193
|
-
const result = await processGenericAccessTokenResponse(as, client, response,
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1356
|
+
async function processAuthorizationCodeOAuth2Response(as, client, response, options) {
|
|
1357
|
+
const result = await processGenericAccessTokenResponse(as, client, response, undefined, options);
|
|
1358
|
+
const claims = getValidatedIdTokenClaims(result);
|
|
1359
|
+
if (claims) {
|
|
1360
|
+
if (client.default_max_age !== undefined) {
|
|
1361
|
+
assertNumber(client.default_max_age, false, '"client.default_max_age"');
|
|
1362
|
+
const now = epochTime() + getClockSkew(client);
|
|
1363
|
+
const tolerance = getClockTolerance(client);
|
|
1364
|
+
if (claims.auth_time + client.default_max_age < now - tolerance) {
|
|
1365
|
+
throw OPE('too much time has elapsed since the last End-User authentication', JWT_TIMESTAMP_CHECK, { claims, now, tolerance, claim: 'auth_time' });
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
if (claims.nonce !== undefined) {
|
|
1369
|
+
throw OPE('unexpected ID Token "nonce" claim value', JWT_CLAIM_COMPARISON, {
|
|
1370
|
+
expected: undefined,
|
|
1371
|
+
claims,
|
|
1372
|
+
claim: 'nonce',
|
|
1373
|
+
});
|
|
1200
1374
|
}
|
|
1201
|
-
delete result.id_token;
|
|
1202
1375
|
}
|
|
1203
1376
|
return result;
|
|
1204
1377
|
}
|
|
1378
|
+
export const WWW_AUTHENTICATE_CHALLENGE = 'OAUTH_WWW_AUTHENTICATE_CHALLENGE';
|
|
1379
|
+
export const RESPONSE_BODY_ERROR = 'OAUTH_RESPONSE_BODY_ERROR';
|
|
1380
|
+
export const UNSUPPORTED_OPERATION = 'OAUTH_UNSUPPORTED_OPERATION';
|
|
1381
|
+
export const AUTHORIZATION_RESPONSE_ERROR = 'OAUTH_AUTHORIZATION_RESPONSE_ERROR';
|
|
1382
|
+
export const JWT_USERINFO_EXPECTED = 'OAUTH_JWT_USERINFO_EXPECTED';
|
|
1383
|
+
export const PARSE_ERROR = 'OAUTH_PARSE_ERROR';
|
|
1384
|
+
export const INVALID_RESPONSE = 'OAUTH_INVALID_RESPONSE';
|
|
1385
|
+
export const INVALID_REQUEST = 'OAUTH_INVALID_REQUEST';
|
|
1386
|
+
export const RESPONSE_IS_NOT_JSON = 'OAUTH_RESPONSE_IS_NOT_JSON';
|
|
1387
|
+
export const RESPONSE_IS_NOT_CONFORM = 'OAUTH_RESPONSE_IS_NOT_CONFORM';
|
|
1388
|
+
export const HTTP_REQUEST_FORBIDDEN = 'OAUTH_HTTP_REQUEST_FORBIDDEN';
|
|
1389
|
+
export const REQUEST_PROTOCOL_FORBIDDEN = 'OAUTH_REQUEST_PROTOCOL_FORBIDDEN';
|
|
1390
|
+
export const JWT_TIMESTAMP_CHECK = 'OAUTH_JWT_TIMESTAMP_CHECK_FAILED';
|
|
1391
|
+
export const JWT_CLAIM_COMPARISON = 'OAUTH_JWT_CLAIM_COMPARISON_FAILED';
|
|
1392
|
+
export const JSON_ATTRIBUTE_COMPARISON = 'OAUTH_JSON_ATTRIBUTE_COMPARISON_FAILED';
|
|
1393
|
+
export const KEY_SELECTION = 'OAUTH_KEY_SELECTION_FAILED';
|
|
1394
|
+
export const MISSING_SERVER_METADATA = 'OAUTH_MISSING_SERVER_METADATA';
|
|
1395
|
+
export const INVALID_SERVER_METADATA = 'OAUTH_INVALID_SERVER_METADATA';
|
|
1205
1396
|
function checkJwtType(expected, result) {
|
|
1206
1397
|
if (typeof result.header.typ !== 'string' || normalizeTyp(result.header.typ) !== expected) {
|
|
1207
|
-
throw
|
|
1398
|
+
throw OPE('unexpected JWT "typ" header parameter value', INVALID_RESPONSE, {
|
|
1399
|
+
header: result.header,
|
|
1400
|
+
});
|
|
1208
1401
|
}
|
|
1209
1402
|
return result;
|
|
1210
1403
|
}
|
|
1211
|
-
export async function clientCredentialsGrantRequest(as, client, parameters, options) {
|
|
1404
|
+
export async function clientCredentialsGrantRequest(as, client, clientAuthentication, parameters, options) {
|
|
1212
1405
|
assertAs(as);
|
|
1213
1406
|
assertClient(client);
|
|
1214
|
-
return tokenEndpointRequest(as, client, 'client_credentials', new URLSearchParams(parameters), options);
|
|
1407
|
+
return tokenEndpointRequest(as, client, clientAuthentication, 'client_credentials', new URLSearchParams(parameters), options);
|
|
1215
1408
|
}
|
|
1216
|
-
export async function genericTokenEndpointRequest(as, client, grantType, parameters, options) {
|
|
1409
|
+
export async function genericTokenEndpointRequest(as, client, clientAuthentication, grantType, parameters, options) {
|
|
1217
1410
|
assertAs(as);
|
|
1218
1411
|
assertClient(client);
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
}
|
|
1222
|
-
return tokenEndpointRequest(as, client, grantType, new URLSearchParams(parameters), options);
|
|
1412
|
+
assertString(grantType, '"grantType"');
|
|
1413
|
+
return tokenEndpointRequest(as, client, clientAuthentication, grantType, new URLSearchParams(parameters), options);
|
|
1223
1414
|
}
|
|
1224
|
-
export async function
|
|
1225
|
-
|
|
1226
|
-
if (isOAuth2Error(result)) {
|
|
1227
|
-
return result;
|
|
1228
|
-
}
|
|
1229
|
-
return result;
|
|
1415
|
+
export async function processGenericTokenEndpointResponse(as, client, response, options) {
|
|
1416
|
+
return processGenericAccessTokenResponse(as, client, response, undefined, options);
|
|
1230
1417
|
}
|
|
1231
|
-
export async function
|
|
1418
|
+
export async function processClientCredentialsResponse(as, client, response, options) {
|
|
1419
|
+
return processGenericAccessTokenResponse(as, client, response, undefined, options);
|
|
1420
|
+
}
|
|
1421
|
+
export async function revocationRequest(as, client, clientAuthentication, token, options) {
|
|
1232
1422
|
assertAs(as);
|
|
1233
1423
|
assertClient(client);
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
}
|
|
1237
|
-
const url = resolveEndpoint(as, 'revocation_endpoint', alias(client, options));
|
|
1424
|
+
assertString(token, '"token"');
|
|
1425
|
+
const url = resolveEndpoint(as, 'revocation_endpoint', client.use_mtls_endpoint_aliases, options?.[allowInsecureRequests] !== true);
|
|
1238
1426
|
const body = new URLSearchParams(options?.additionalParameters);
|
|
1239
1427
|
body.set('token', token);
|
|
1240
1428
|
const headers = prepareHeaders(options?.headers);
|
|
1241
1429
|
headers.delete('accept');
|
|
1242
|
-
return authenticatedRequest(as, client,
|
|
1430
|
+
return authenticatedRequest(as, client, clientAuthentication, url, body, headers, options);
|
|
1243
1431
|
}
|
|
1244
1432
|
export async function processRevocationResponse(response) {
|
|
1245
1433
|
if (!looseInstanceOf(response, Response)) {
|
|
1246
|
-
throw
|
|
1434
|
+
throw CodedTypeError('"response" must be an instance of Response', ERR_INVALID_ARG_TYPE);
|
|
1435
|
+
}
|
|
1436
|
+
let challenges;
|
|
1437
|
+
if ((challenges = parseWwwAuthenticateChallenges(response))) {
|
|
1438
|
+
throw new WWWAuthenticateChallengeError('server responded with a challenge in the WWW-Authenticate HTTP Header', { cause: challenges, response });
|
|
1247
1439
|
}
|
|
1248
1440
|
if (response.status !== 200) {
|
|
1249
1441
|
let err;
|
|
1250
1442
|
if ((err = await handleOAuthBodyError(response))) {
|
|
1251
|
-
|
|
1443
|
+
await response.body?.cancel();
|
|
1444
|
+
throw new ResponseBodyError('server responded with an error in the response body', {
|
|
1445
|
+
cause: err,
|
|
1446
|
+
response,
|
|
1447
|
+
});
|
|
1252
1448
|
}
|
|
1253
|
-
throw
|
|
1449
|
+
throw OPE('"response" is not a conform Revocation Endpoint response', RESPONSE_IS_NOT_CONFORM, response);
|
|
1254
1450
|
}
|
|
1255
1451
|
return undefined;
|
|
1256
1452
|
}
|
|
1257
1453
|
function assertReadableResponse(response) {
|
|
1258
1454
|
if (response.bodyUsed) {
|
|
1259
|
-
throw
|
|
1455
|
+
throw CodedTypeError('"response" body has been used already', ERR_INVALID_ARG_VALUE);
|
|
1260
1456
|
}
|
|
1261
1457
|
}
|
|
1262
|
-
export async function introspectionRequest(as, client, token, options) {
|
|
1458
|
+
export async function introspectionRequest(as, client, clientAuthentication, token, options) {
|
|
1263
1459
|
assertAs(as);
|
|
1264
1460
|
assertClient(client);
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
}
|
|
1268
|
-
const url = resolveEndpoint(as, 'introspection_endpoint', alias(client, options));
|
|
1461
|
+
assertString(token, '"token"');
|
|
1462
|
+
const url = resolveEndpoint(as, 'introspection_endpoint', client.use_mtls_endpoint_aliases, options?.[allowInsecureRequests] !== true);
|
|
1269
1463
|
const body = new URLSearchParams(options?.additionalParameters);
|
|
1270
1464
|
body.set('token', token);
|
|
1271
1465
|
const headers = prepareHeaders(options?.headers);
|
|
@@ -1275,109 +1469,113 @@ export async function introspectionRequest(as, client, token, options) {
|
|
|
1275
1469
|
else {
|
|
1276
1470
|
headers.set('accept', 'application/json');
|
|
1277
1471
|
}
|
|
1278
|
-
return authenticatedRequest(as, client,
|
|
1472
|
+
return authenticatedRequest(as, client, clientAuthentication, url, body, headers, options);
|
|
1279
1473
|
}
|
|
1280
|
-
export async function processIntrospectionResponse(as, client, response) {
|
|
1474
|
+
export async function processIntrospectionResponse(as, client, response, options) {
|
|
1281
1475
|
assertAs(as);
|
|
1282
1476
|
assertClient(client);
|
|
1283
1477
|
if (!looseInstanceOf(response, Response)) {
|
|
1284
|
-
throw
|
|
1478
|
+
throw CodedTypeError('"response" must be an instance of Response', ERR_INVALID_ARG_TYPE);
|
|
1479
|
+
}
|
|
1480
|
+
let challenges;
|
|
1481
|
+
if ((challenges = parseWwwAuthenticateChallenges(response))) {
|
|
1482
|
+
throw new WWWAuthenticateChallengeError('server responded with a challenge in the WWW-Authenticate HTTP Header', { cause: challenges, response });
|
|
1285
1483
|
}
|
|
1286
1484
|
if (response.status !== 200) {
|
|
1287
1485
|
let err;
|
|
1288
1486
|
if ((err = await handleOAuthBodyError(response))) {
|
|
1289
|
-
|
|
1487
|
+
await response.body?.cancel();
|
|
1488
|
+
throw new ResponseBodyError('server responded with an error in the response body', {
|
|
1489
|
+
cause: err,
|
|
1490
|
+
response,
|
|
1491
|
+
});
|
|
1290
1492
|
}
|
|
1291
|
-
throw
|
|
1493
|
+
throw OPE('"response" is not a conform Introspection Endpoint response', RESPONSE_IS_NOT_CONFORM, response);
|
|
1292
1494
|
}
|
|
1293
1495
|
let json;
|
|
1294
1496
|
if (getContentType(response) === 'application/token-introspection+jwt') {
|
|
1295
1497
|
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
|
|
1498
|
+
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
1499
|
.then(checkJwtType.bind(undefined, 'token-introspection+jwt'))
|
|
1298
1500
|
.then(validatePresence.bind(undefined, ['aud', 'iat', 'iss']))
|
|
1299
|
-
.then(validateIssuer.bind(undefined, as
|
|
1501
|
+
.then(validateIssuer.bind(undefined, as))
|
|
1300
1502
|
.then(validateAudience.bind(undefined, client.client_id));
|
|
1301
|
-
|
|
1503
|
+
jwtRefs.set(response, jwt);
|
|
1302
1504
|
json = claims.token_introspection;
|
|
1303
1505
|
if (!isJsonObject(json)) {
|
|
1304
|
-
throw
|
|
1506
|
+
throw OPE('JWT "token_introspection" claim must be a JSON object', INVALID_RESPONSE, {
|
|
1507
|
+
claims,
|
|
1508
|
+
});
|
|
1305
1509
|
}
|
|
1306
1510
|
}
|
|
1307
1511
|
else {
|
|
1308
1512
|
assertReadableResponse(response);
|
|
1513
|
+
assertApplicationJson(response);
|
|
1309
1514
|
try {
|
|
1310
1515
|
json = await response.json();
|
|
1311
1516
|
}
|
|
1312
1517
|
catch (cause) {
|
|
1313
|
-
throw
|
|
1518
|
+
throw OPE('failed to parse "response" body as JSON', PARSE_ERROR, cause);
|
|
1314
1519
|
}
|
|
1315
1520
|
if (!isJsonObject(json)) {
|
|
1316
|
-
throw
|
|
1521
|
+
throw OPE('"response" body must be a top level object', INVALID_RESPONSE, { body: json });
|
|
1317
1522
|
}
|
|
1318
1523
|
}
|
|
1319
1524
|
if (typeof json.active !== 'boolean') {
|
|
1320
|
-
throw
|
|
1525
|
+
throw OPE('"response" body "active" property must be a boolean', INVALID_RESPONSE, {
|
|
1526
|
+
body: json,
|
|
1527
|
+
});
|
|
1321
1528
|
}
|
|
1322
1529
|
return json;
|
|
1323
1530
|
}
|
|
1324
1531
|
async function jwksRequest(as, options) {
|
|
1325
1532
|
assertAs(as);
|
|
1326
|
-
const url = resolveEndpoint(as, 'jwks_uri');
|
|
1533
|
+
const url = resolveEndpoint(as, 'jwks_uri', false, options?.[allowInsecureRequests] !== true);
|
|
1327
1534
|
const headers = prepareHeaders(options?.headers);
|
|
1328
1535
|
headers.set('accept', 'application/json');
|
|
1329
1536
|
headers.append('accept', 'application/jwk-set+json');
|
|
1330
1537
|
return (options?.[customFetch] || fetch)(url.href, {
|
|
1538
|
+
body: undefined,
|
|
1331
1539
|
headers: Object.fromEntries(headers.entries()),
|
|
1332
1540
|
method: 'GET',
|
|
1333
1541
|
redirect: 'manual',
|
|
1334
|
-
signal: options?.signal ? signal(options.signal) :
|
|
1335
|
-
})
|
|
1542
|
+
signal: options?.signal ? signal(options.signal) : undefined,
|
|
1543
|
+
});
|
|
1336
1544
|
}
|
|
1337
1545
|
async function processJwksResponse(response) {
|
|
1338
1546
|
if (!looseInstanceOf(response, Response)) {
|
|
1339
|
-
throw
|
|
1547
|
+
throw CodedTypeError('"response" must be an instance of Response', ERR_INVALID_ARG_TYPE);
|
|
1340
1548
|
}
|
|
1341
1549
|
if (response.status !== 200) {
|
|
1342
|
-
throw
|
|
1550
|
+
throw OPE('"response" is not a conform JSON Web Key Set response', RESPONSE_IS_NOT_CONFORM, response);
|
|
1343
1551
|
}
|
|
1344
1552
|
assertReadableResponse(response);
|
|
1553
|
+
assertContentTypes(response, 'application/json', 'application/jwk-set+json');
|
|
1345
1554
|
let json;
|
|
1346
1555
|
try {
|
|
1347
1556
|
json = await response.json();
|
|
1348
1557
|
}
|
|
1349
1558
|
catch (cause) {
|
|
1350
|
-
throw
|
|
1559
|
+
throw OPE('failed to parse "response" body as JSON', PARSE_ERROR, cause);
|
|
1351
1560
|
}
|
|
1352
1561
|
if (!isJsonObject(json)) {
|
|
1353
|
-
throw
|
|
1562
|
+
throw OPE('"response" body must be a top level object', INVALID_RESPONSE, { body: json });
|
|
1354
1563
|
}
|
|
1355
1564
|
if (!Array.isArray(json.keys)) {
|
|
1356
|
-
throw
|
|
1565
|
+
throw OPE('"response" body "keys" property must be an array', INVALID_RESPONSE, { body: json });
|
|
1357
1566
|
}
|
|
1358
1567
|
if (!Array.prototype.every.call(json.keys, isJsonObject)) {
|
|
1359
|
-
throw
|
|
1568
|
+
throw OPE('"response" body "keys" property members must be JWK formatted objects', INVALID_RESPONSE, { body: json });
|
|
1360
1569
|
}
|
|
1361
1570
|
return json;
|
|
1362
1571
|
}
|
|
1363
1572
|
async function handleOAuthBodyError(response) {
|
|
1364
1573
|
if (response.status > 399 && response.status < 500) {
|
|
1365
1574
|
assertReadableResponse(response);
|
|
1575
|
+
assertApplicationJson(response);
|
|
1366
1576
|
try {
|
|
1367
|
-
const json = await response.json();
|
|
1577
|
+
const json = await response.clone().json();
|
|
1368
1578
|
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
1579
|
return json;
|
|
1382
1580
|
}
|
|
1383
1581
|
}
|
|
@@ -1385,19 +1583,42 @@ async function handleOAuthBodyError(response) {
|
|
|
1385
1583
|
}
|
|
1386
1584
|
return undefined;
|
|
1387
1585
|
}
|
|
1388
|
-
function
|
|
1389
|
-
|
|
1390
|
-
|
|
1586
|
+
function supported(alg) {
|
|
1587
|
+
switch (alg) {
|
|
1588
|
+
case 'PS256':
|
|
1589
|
+
case 'ES256':
|
|
1590
|
+
case 'RS256':
|
|
1591
|
+
case 'PS384':
|
|
1592
|
+
case 'ES384':
|
|
1593
|
+
case 'RS384':
|
|
1594
|
+
case 'PS512':
|
|
1595
|
+
case 'ES512':
|
|
1596
|
+
case 'RS512':
|
|
1597
|
+
case 'Ed25519':
|
|
1598
|
+
case 'EdDSA':
|
|
1599
|
+
return true;
|
|
1600
|
+
default:
|
|
1601
|
+
return false;
|
|
1391
1602
|
}
|
|
1392
|
-
return alg;
|
|
1393
1603
|
}
|
|
1394
|
-
function
|
|
1604
|
+
function checkSupportedJwsAlg(header) {
|
|
1605
|
+
if (!supported(header.alg)) {
|
|
1606
|
+
throw new UnsupportedOperationError('unsupported JWS "alg" identifier', {
|
|
1607
|
+
cause: { alg: header.alg },
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
function checkRsaKeyAlgorithm(key) {
|
|
1612
|
+
const { algorithm } = key;
|
|
1395
1613
|
if (typeof algorithm.modulusLength !== 'number' || algorithm.modulusLength < 2048) {
|
|
1396
|
-
throw new
|
|
1614
|
+
throw new UnsupportedOperationError(`unsupported ${algorithm.name} modulusLength`, {
|
|
1615
|
+
cause: key,
|
|
1616
|
+
});
|
|
1397
1617
|
}
|
|
1398
1618
|
}
|
|
1399
|
-
function ecdsaHashName(
|
|
1400
|
-
|
|
1619
|
+
function ecdsaHashName(key) {
|
|
1620
|
+
const { algorithm } = key;
|
|
1621
|
+
switch (algorithm.namedCurve) {
|
|
1401
1622
|
case 'P-256':
|
|
1402
1623
|
return 'SHA-256';
|
|
1403
1624
|
case 'P-384':
|
|
@@ -1405,7 +1626,7 @@ function ecdsaHashName(namedCurve) {
|
|
|
1405
1626
|
case 'P-521':
|
|
1406
1627
|
return 'SHA-512';
|
|
1407
1628
|
default:
|
|
1408
|
-
throw new UnsupportedOperationError();
|
|
1629
|
+
throw new UnsupportedOperationError('unsupported ECDSA namedCurve', { cause: key });
|
|
1409
1630
|
}
|
|
1410
1631
|
}
|
|
1411
1632
|
function keyToSubtle(key) {
|
|
@@ -1413,10 +1634,10 @@ function keyToSubtle(key) {
|
|
|
1413
1634
|
case 'ECDSA':
|
|
1414
1635
|
return {
|
|
1415
1636
|
name: key.algorithm.name,
|
|
1416
|
-
hash: ecdsaHashName(key
|
|
1637
|
+
hash: ecdsaHashName(key),
|
|
1417
1638
|
};
|
|
1418
1639
|
case 'RSA-PSS': {
|
|
1419
|
-
checkRsaKeyAlgorithm(key
|
|
1640
|
+
checkRsaKeyAlgorithm(key);
|
|
1420
1641
|
switch (key.algorithm.hash.name) {
|
|
1421
1642
|
case 'SHA-256':
|
|
1422
1643
|
case 'SHA-384':
|
|
@@ -1426,103 +1647,109 @@ function keyToSubtle(key) {
|
|
|
1426
1647
|
saltLength: parseInt(key.algorithm.hash.name.slice(-3), 10) >> 3,
|
|
1427
1648
|
};
|
|
1428
1649
|
default:
|
|
1429
|
-
throw new UnsupportedOperationError();
|
|
1650
|
+
throw new UnsupportedOperationError('unsupported RSA-PSS hash name', { cause: key });
|
|
1430
1651
|
}
|
|
1431
1652
|
}
|
|
1432
1653
|
case 'RSASSA-PKCS1-v1_5':
|
|
1433
|
-
checkRsaKeyAlgorithm(key
|
|
1654
|
+
checkRsaKeyAlgorithm(key);
|
|
1434
1655
|
return key.algorithm.name;
|
|
1435
|
-
case 'Ed448':
|
|
1436
1656
|
case 'Ed25519':
|
|
1657
|
+
case 'EdDSA':
|
|
1437
1658
|
return key.algorithm.name;
|
|
1438
1659
|
}
|
|
1439
|
-
throw new UnsupportedOperationError();
|
|
1660
|
+
throw new UnsupportedOperationError('unsupported CryptoKey algorithm name', { cause: key });
|
|
1440
1661
|
}
|
|
1441
|
-
const noSignatureCheck = Symbol();
|
|
1442
1662
|
async function validateJwsSignature(protectedHeader, payload, key, signature) {
|
|
1443
|
-
const
|
|
1444
|
-
const
|
|
1663
|
+
const data = buf(`${protectedHeader}.${payload}`);
|
|
1664
|
+
const algorithm = keyToSubtle(key);
|
|
1665
|
+
const verified = await crypto.subtle.verify(algorithm, key, signature, data);
|
|
1445
1666
|
if (!verified) {
|
|
1446
|
-
throw
|
|
1667
|
+
throw OPE('JWT signature verification failed', INVALID_RESPONSE, {
|
|
1668
|
+
key,
|
|
1669
|
+
data,
|
|
1670
|
+
signature,
|
|
1671
|
+
algorithm,
|
|
1672
|
+
});
|
|
1447
1673
|
}
|
|
1448
1674
|
}
|
|
1449
|
-
async function validateJwt(jws, checkAlg,
|
|
1450
|
-
let { 0: protectedHeader, 1: payload,
|
|
1675
|
+
async function validateJwt(jws, checkAlg, clockSkew, clockTolerance, decryptJwt) {
|
|
1676
|
+
let { 0: protectedHeader, 1: payload, length } = jws.split('.');
|
|
1451
1677
|
if (length === 5) {
|
|
1452
1678
|
if (decryptJwt !== undefined) {
|
|
1453
1679
|
jws = await decryptJwt(jws);
|
|
1454
|
-
({ 0: protectedHeader, 1: payload,
|
|
1680
|
+
({ 0: protectedHeader, 1: payload, length } = jws.split('.'));
|
|
1455
1681
|
}
|
|
1456
1682
|
else {
|
|
1457
|
-
throw new UnsupportedOperationError('JWE
|
|
1683
|
+
throw new UnsupportedOperationError('JWE decryption is not configured', { cause: jws });
|
|
1458
1684
|
}
|
|
1459
1685
|
}
|
|
1460
1686
|
if (length !== 3) {
|
|
1461
|
-
throw
|
|
1687
|
+
throw OPE('Invalid JWT', INVALID_RESPONSE, jws);
|
|
1462
1688
|
}
|
|
1463
1689
|
let header;
|
|
1464
1690
|
try {
|
|
1465
1691
|
header = JSON.parse(buf(b64u(protectedHeader)));
|
|
1466
1692
|
}
|
|
1467
1693
|
catch (cause) {
|
|
1468
|
-
throw
|
|
1694
|
+
throw OPE('failed to parse JWT Header body as base64url encoded JSON', PARSE_ERROR, cause);
|
|
1469
1695
|
}
|
|
1470
1696
|
if (!isJsonObject(header)) {
|
|
1471
|
-
throw
|
|
1697
|
+
throw OPE('JWT Header must be a top level object', INVALID_RESPONSE, jws);
|
|
1472
1698
|
}
|
|
1473
1699
|
checkAlg(header);
|
|
1474
1700
|
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);
|
|
1701
|
+
throw new UnsupportedOperationError('no JWT "crit" header parameter extensions are supported', {
|
|
1702
|
+
cause: { header },
|
|
1703
|
+
});
|
|
1482
1704
|
}
|
|
1483
1705
|
let claims;
|
|
1484
1706
|
try {
|
|
1485
1707
|
claims = JSON.parse(buf(b64u(payload)));
|
|
1486
1708
|
}
|
|
1487
1709
|
catch (cause) {
|
|
1488
|
-
throw
|
|
1710
|
+
throw OPE('failed to parse JWT Payload body as base64url encoded JSON', PARSE_ERROR, cause);
|
|
1489
1711
|
}
|
|
1490
1712
|
if (!isJsonObject(claims)) {
|
|
1491
|
-
throw
|
|
1713
|
+
throw OPE('JWT Payload must be a top level object', INVALID_RESPONSE, jws);
|
|
1492
1714
|
}
|
|
1493
1715
|
const now = epochTime() + clockSkew;
|
|
1494
1716
|
if (claims.exp !== undefined) {
|
|
1495
1717
|
if (typeof claims.exp !== 'number') {
|
|
1496
|
-
throw
|
|
1718
|
+
throw OPE('unexpected JWT "exp" (expiration time) claim type', INVALID_RESPONSE, { claims });
|
|
1497
1719
|
}
|
|
1498
1720
|
if (claims.exp <= now - clockTolerance) {
|
|
1499
|
-
throw
|
|
1721
|
+
throw OPE('unexpected JWT "exp" (expiration time) claim value, expiration is past current timestamp', JWT_TIMESTAMP_CHECK, { claims, now, tolerance: clockTolerance, claim: 'exp' });
|
|
1500
1722
|
}
|
|
1501
1723
|
}
|
|
1502
1724
|
if (claims.iat !== undefined) {
|
|
1503
1725
|
if (typeof claims.iat !== 'number') {
|
|
1504
|
-
throw
|
|
1726
|
+
throw OPE('unexpected JWT "iat" (issued at) claim type', INVALID_RESPONSE, { claims });
|
|
1505
1727
|
}
|
|
1506
1728
|
}
|
|
1507
1729
|
if (claims.iss !== undefined) {
|
|
1508
1730
|
if (typeof claims.iss !== 'string') {
|
|
1509
|
-
throw
|
|
1731
|
+
throw OPE('unexpected JWT "iss" (issuer) claim type', INVALID_RESPONSE, { claims });
|
|
1510
1732
|
}
|
|
1511
1733
|
}
|
|
1512
1734
|
if (claims.nbf !== undefined) {
|
|
1513
1735
|
if (typeof claims.nbf !== 'number') {
|
|
1514
|
-
throw
|
|
1736
|
+
throw OPE('unexpected JWT "nbf" (not before) claim type', INVALID_RESPONSE, { claims });
|
|
1515
1737
|
}
|
|
1516
1738
|
if (claims.nbf > now + clockTolerance) {
|
|
1517
|
-
throw
|
|
1739
|
+
throw OPE('unexpected JWT "nbf" (not before) claim value', JWT_TIMESTAMP_CHECK, {
|
|
1740
|
+
claims,
|
|
1741
|
+
now,
|
|
1742
|
+
tolerance: clockTolerance,
|
|
1743
|
+
claim: 'nbf',
|
|
1744
|
+
});
|
|
1518
1745
|
}
|
|
1519
1746
|
}
|
|
1520
1747
|
if (claims.aud !== undefined) {
|
|
1521
1748
|
if (typeof claims.aud !== 'string' && !Array.isArray(claims.aud)) {
|
|
1522
|
-
throw
|
|
1749
|
+
throw OPE('unexpected JWT "aud" (audience) claim type', INVALID_RESPONSE, { claims });
|
|
1523
1750
|
}
|
|
1524
1751
|
}
|
|
1525
|
-
return { header, claims,
|
|
1752
|
+
return { header, claims, jwt: jws };
|
|
1526
1753
|
}
|
|
1527
1754
|
export async function validateJwtAuthResponse(as, client, parameters, expectedState, options) {
|
|
1528
1755
|
assertAs(as);
|
|
@@ -1531,16 +1758,20 @@ export async function validateJwtAuthResponse(as, client, parameters, expectedSt
|
|
|
1531
1758
|
parameters = parameters.searchParams;
|
|
1532
1759
|
}
|
|
1533
1760
|
if (!(parameters instanceof URLSearchParams)) {
|
|
1534
|
-
throw
|
|
1761
|
+
throw CodedTypeError('"parameters" must be an instance of URLSearchParams, or URL', ERR_INVALID_ARG_TYPE);
|
|
1535
1762
|
}
|
|
1536
1763
|
const response = getURLSearchParameter(parameters, 'response');
|
|
1537
1764
|
if (!response) {
|
|
1538
|
-
throw
|
|
1765
|
+
throw OPE('"parameters" does not contain a JARM response', INVALID_RESPONSE);
|
|
1539
1766
|
}
|
|
1540
|
-
const { claims } = await validateJwt(response, checkSigningAlgorithm.bind(undefined, client.authorization_signed_response_alg, as.authorization_signing_alg_values_supported
|
|
1767
|
+
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
1768
|
.then(validatePresence.bind(undefined, ['aud', 'exp', 'iss']))
|
|
1542
|
-
.then(validateIssuer.bind(undefined, as
|
|
1769
|
+
.then(validateIssuer.bind(undefined, as))
|
|
1543
1770
|
.then(validateAudience.bind(undefined, client.client_id));
|
|
1771
|
+
const { 0: protectedHeader, 1: payload, 2: encodedSignature } = jwt.split('.');
|
|
1772
|
+
const signature = b64u(encodedSignature);
|
|
1773
|
+
const key = await getPublicSigKeyFromIssuerJwksUri(as, options, header);
|
|
1774
|
+
await validateJwsSignature(protectedHeader, payload, key, signature);
|
|
1544
1775
|
const result = new URLSearchParams();
|
|
1545
1776
|
for (const [key, value] of Object.entries(claims)) {
|
|
1546
1777
|
if (typeof value === 'string' && key !== 'aud') {
|
|
@@ -1549,9 +1780,9 @@ export async function validateJwtAuthResponse(as, client, parameters, expectedSt
|
|
|
1549
1780
|
}
|
|
1550
1781
|
return validateAuthResponse(as, client, result, expectedState);
|
|
1551
1782
|
}
|
|
1552
|
-
async function idTokenHash(
|
|
1783
|
+
async function idTokenHash(data, header, claimName) {
|
|
1553
1784
|
let algorithm;
|
|
1554
|
-
switch (alg) {
|
|
1785
|
+
switch (header.alg) {
|
|
1555
1786
|
case 'RS256':
|
|
1556
1787
|
case 'PS256':
|
|
1557
1788
|
case 'ES256':
|
|
@@ -1565,35 +1796,37 @@ async function idTokenHash(alg, data, key) {
|
|
|
1565
1796
|
case 'RS512':
|
|
1566
1797
|
case 'PS512':
|
|
1567
1798
|
case 'ES512':
|
|
1799
|
+
case 'Ed25519':
|
|
1800
|
+
case 'EdDSA':
|
|
1568
1801
|
algorithm = 'SHA-512';
|
|
1569
1802
|
break;
|
|
1570
|
-
case 'EdDSA':
|
|
1571
|
-
if (key.algorithm.name === 'Ed25519') {
|
|
1572
|
-
algorithm = 'SHA-512';
|
|
1573
|
-
break;
|
|
1574
|
-
}
|
|
1575
|
-
throw new UnsupportedOperationError();
|
|
1576
1803
|
default:
|
|
1577
|
-
throw new UnsupportedOperationError();
|
|
1804
|
+
throw new UnsupportedOperationError(`unsupported JWS algorithm for ${claimName} calculation`, { cause: { alg: header.alg } });
|
|
1578
1805
|
}
|
|
1579
1806
|
const digest = await crypto.subtle.digest(algorithm, buf(data));
|
|
1580
1807
|
return b64u(digest.slice(0, digest.byteLength / 2));
|
|
1581
1808
|
}
|
|
1582
|
-
async function idTokenHashMatches(data, actual,
|
|
1583
|
-
const expected = await idTokenHash(
|
|
1809
|
+
async function idTokenHashMatches(data, actual, header, claimName) {
|
|
1810
|
+
const expected = await idTokenHash(data, header, claimName);
|
|
1584
1811
|
return actual === expected;
|
|
1585
1812
|
}
|
|
1586
1813
|
export async function validateDetachedSignatureResponse(as, client, parameters, expectedNonce, expectedState, maxAge, options) {
|
|
1814
|
+
return validateHybridResponse(as, client, parameters, expectedNonce, expectedState, maxAge, options, true);
|
|
1815
|
+
}
|
|
1816
|
+
export async function validateCodeIdTokenResponse(as, client, parameters, expectedNonce, expectedState, maxAge, options) {
|
|
1817
|
+
return validateHybridResponse(as, client, parameters, expectedNonce, expectedState, maxAge, options, false);
|
|
1818
|
+
}
|
|
1819
|
+
async function validateHybridResponse(as, client, parameters, expectedNonce, expectedState, maxAge, options, fapi) {
|
|
1587
1820
|
assertAs(as);
|
|
1588
1821
|
assertClient(client);
|
|
1589
1822
|
if (parameters instanceof URL) {
|
|
1590
1823
|
if (!parameters.hash.length) {
|
|
1591
|
-
throw
|
|
1824
|
+
throw CodedTypeError('"parameters" as an instance of URL must contain a hash (fragment) with the Authorization Response parameters', ERR_INVALID_ARG_VALUE);
|
|
1592
1825
|
}
|
|
1593
1826
|
parameters = new URLSearchParams(parameters.hash.slice(1));
|
|
1594
1827
|
}
|
|
1595
1828
|
if (!(parameters instanceof URLSearchParams)) {
|
|
1596
|
-
throw
|
|
1829
|
+
throw CodedTypeError('"parameters" must be an instance of URLSearchParams', ERR_INVALID_ARG_TYPE);
|
|
1597
1830
|
}
|
|
1598
1831
|
parameters = new URLSearchParams(parameters);
|
|
1599
1832
|
const id_token = getURLSearchParameter(parameters, 'id_token');
|
|
@@ -1603,23 +1836,18 @@ export async function validateDetachedSignatureResponse(as, client, parameters,
|
|
|
1603
1836
|
case expectNoState:
|
|
1604
1837
|
break;
|
|
1605
1838
|
default:
|
|
1606
|
-
|
|
1607
|
-
throw new TypeError('"expectedState" must be a non-empty string');
|
|
1608
|
-
}
|
|
1839
|
+
assertString(expectedState, '"expectedState" argument');
|
|
1609
1840
|
}
|
|
1610
1841
|
const result = validateAuthResponse({
|
|
1611
1842
|
...as,
|
|
1612
1843
|
authorization_response_iss_parameter_supported: false,
|
|
1613
1844
|
}, client, parameters, expectedState);
|
|
1614
|
-
if (isOAuth2Error(result)) {
|
|
1615
|
-
return result;
|
|
1616
|
-
}
|
|
1617
1845
|
if (!id_token) {
|
|
1618
|
-
throw
|
|
1846
|
+
throw OPE('"parameters" does not contain an ID Token', INVALID_RESPONSE);
|
|
1619
1847
|
}
|
|
1620
1848
|
const code = getURLSearchParameter(parameters, 'code');
|
|
1621
1849
|
if (!code) {
|
|
1622
|
-
throw
|
|
1850
|
+
throw OPE('"parameters" does not contain an Authorization Code', INVALID_RESPONSE);
|
|
1623
1851
|
}
|
|
1624
1852
|
const requiredClaims = [
|
|
1625
1853
|
'aud',
|
|
@@ -1630,86 +1858,132 @@ export async function validateDetachedSignatureResponse(as, client, parameters,
|
|
|
1630
1858
|
'nonce',
|
|
1631
1859
|
'c_hash',
|
|
1632
1860
|
];
|
|
1633
|
-
|
|
1861
|
+
const state = parameters.get('state');
|
|
1862
|
+
if (fapi && (typeof expectedState === 'string' || state !== null)) {
|
|
1634
1863
|
requiredClaims.push('s_hash');
|
|
1635
1864
|
}
|
|
1636
|
-
|
|
1865
|
+
if (maxAge !== undefined) {
|
|
1866
|
+
assertNumber(maxAge, false, '"maxAge" argument');
|
|
1867
|
+
}
|
|
1868
|
+
else if (client.default_max_age !== undefined) {
|
|
1869
|
+
assertNumber(client.default_max_age, false, '"client.default_max_age"');
|
|
1870
|
+
}
|
|
1871
|
+
maxAge ??= client.default_max_age ?? skipAuthTimeCheck;
|
|
1872
|
+
if (client.require_auth_time || maxAge !== skipAuthTimeCheck) {
|
|
1873
|
+
requiredClaims.push('auth_time');
|
|
1874
|
+
}
|
|
1875
|
+
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
1876
|
.then(validatePresence.bind(undefined, requiredClaims))
|
|
1638
|
-
.then(validateIssuer.bind(undefined, as
|
|
1877
|
+
.then(validateIssuer.bind(undefined, as))
|
|
1639
1878
|
.then(validateAudience.bind(undefined, client.client_id));
|
|
1640
1879
|
const clockSkew = getClockSkew(client);
|
|
1641
1880
|
const now = epochTime() + clockSkew;
|
|
1642
1881
|
if (claims.iat < now - 3600) {
|
|
1643
|
-
throw
|
|
1882
|
+
throw OPE('unexpected JWT "iat" (issued at) claim value, it is too far in the past', JWT_TIMESTAMP_CHECK, { now, claims, claim: 'iat' });
|
|
1644
1883
|
}
|
|
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');
|
|
1884
|
+
assertString(claims.c_hash, 'ID Token "c_hash" (code hash) claim value', INVALID_RESPONSE, {
|
|
1885
|
+
claims,
|
|
1886
|
+
});
|
|
1887
|
+
if (claims.auth_time !== undefined) {
|
|
1888
|
+
assertNumber(claims.auth_time, false, 'ID Token "auth_time" (authentication time)', INVALID_RESPONSE, { claims });
|
|
1665
1889
|
}
|
|
1666
1890
|
if (maxAge !== skipAuthTimeCheck) {
|
|
1667
|
-
if (typeof maxAge !== 'number' || maxAge < 0) {
|
|
1668
|
-
throw new TypeError('"maxAge" must be a non-negative number');
|
|
1669
|
-
}
|
|
1670
1891
|
const now = epochTime() + getClockSkew(client);
|
|
1671
1892
|
const tolerance = getClockTolerance(client);
|
|
1672
1893
|
if (claims.auth_time + maxAge < now - tolerance) {
|
|
1673
|
-
throw
|
|
1894
|
+
throw OPE('too much time has elapsed since the last End-User authentication', JWT_TIMESTAMP_CHECK, { claims, now, tolerance, claim: 'auth_time' });
|
|
1674
1895
|
}
|
|
1675
1896
|
}
|
|
1676
|
-
|
|
1677
|
-
throw new TypeError('"expectedNonce" must be a non-empty string');
|
|
1678
|
-
}
|
|
1897
|
+
assertString(expectedNonce, '"expectedNonce" argument');
|
|
1679
1898
|
if (claims.nonce !== expectedNonce) {
|
|
1680
|
-
throw
|
|
1899
|
+
throw OPE('unexpected ID Token "nonce" claim value', JWT_CLAIM_COMPARISON, {
|
|
1900
|
+
expected: expectedNonce,
|
|
1901
|
+
claims,
|
|
1902
|
+
claim: 'nonce',
|
|
1903
|
+
});
|
|
1681
1904
|
}
|
|
1682
1905
|
if (Array.isArray(claims.aud) && claims.aud.length !== 1) {
|
|
1683
1906
|
if (claims.azp === undefined) {
|
|
1684
|
-
throw
|
|
1907
|
+
throw OPE('ID Token "aud" (audience) claim includes additional untrusted audiences', JWT_CLAIM_COMPARISON, { claims, claim: 'aud' });
|
|
1685
1908
|
}
|
|
1686
1909
|
if (claims.azp !== client.client_id) {
|
|
1687
|
-
throw
|
|
1910
|
+
throw OPE('unexpected ID Token "azp" (authorized party) claim value', JWT_CLAIM_COMPARISON, {
|
|
1911
|
+
expected: client.client_id,
|
|
1912
|
+
claims,
|
|
1913
|
+
claim: 'azp',
|
|
1914
|
+
});
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
const { 0: protectedHeader, 1: payload, 2: encodedSignature } = jwt.split('.');
|
|
1918
|
+
const signature = b64u(encodedSignature);
|
|
1919
|
+
const key = await getPublicSigKeyFromIssuerJwksUri(as, options, header);
|
|
1920
|
+
await validateJwsSignature(protectedHeader, payload, key, signature);
|
|
1921
|
+
if ((await idTokenHashMatches(code, claims.c_hash, header, 'c_hash')) !== true) {
|
|
1922
|
+
throw OPE('invalid ID Token "c_hash" (code hash) claim value', JWT_CLAIM_COMPARISON, {
|
|
1923
|
+
code,
|
|
1924
|
+
alg: header.alg,
|
|
1925
|
+
claim: 'c_hash',
|
|
1926
|
+
claims,
|
|
1927
|
+
});
|
|
1928
|
+
}
|
|
1929
|
+
if ((fapi && state !== null) || claims.s_hash !== undefined) {
|
|
1930
|
+
assertString(claims.s_hash, 'ID Token "s_hash" (state hash) claim value', INVALID_RESPONSE, {
|
|
1931
|
+
claims,
|
|
1932
|
+
});
|
|
1933
|
+
assertString(state, '"state" response parameter', INVALID_RESPONSE, { parameters });
|
|
1934
|
+
if ((await idTokenHashMatches(state, claims.s_hash, header, 's_hash')) !== true) {
|
|
1935
|
+
throw OPE('invalid ID Token "s_hash" (state hash) claim value', JWT_CLAIM_COMPARISON, {
|
|
1936
|
+
state,
|
|
1937
|
+
alg: header.alg,
|
|
1938
|
+
claim: 's_hash',
|
|
1939
|
+
claims,
|
|
1940
|
+
});
|
|
1688
1941
|
}
|
|
1689
1942
|
}
|
|
1690
1943
|
return result;
|
|
1691
1944
|
}
|
|
1692
|
-
|
|
1945
|
+
const SUPPORTED_JWS_ALGS = Symbol();
|
|
1946
|
+
function checkSigningAlgorithm(client, issuer, fallback, header) {
|
|
1693
1947
|
if (client !== undefined) {
|
|
1694
|
-
if (header.alg !== client) {
|
|
1695
|
-
throw
|
|
1948
|
+
if (typeof client === 'string' ? header.alg !== client : !client.includes(header.alg)) {
|
|
1949
|
+
throw OPE('unexpected JWT "alg" header parameter', INVALID_RESPONSE, {
|
|
1950
|
+
header,
|
|
1951
|
+
expected: client,
|
|
1952
|
+
reason: 'client configuration',
|
|
1953
|
+
});
|
|
1696
1954
|
}
|
|
1697
1955
|
return;
|
|
1698
1956
|
}
|
|
1699
1957
|
if (Array.isArray(issuer)) {
|
|
1700
1958
|
if (!issuer.includes(header.alg)) {
|
|
1701
|
-
throw
|
|
1959
|
+
throw OPE('unexpected JWT "alg" header parameter', INVALID_RESPONSE, {
|
|
1960
|
+
header,
|
|
1961
|
+
expected: issuer,
|
|
1962
|
+
reason: 'authorization server metadata',
|
|
1963
|
+
});
|
|
1702
1964
|
}
|
|
1703
1965
|
return;
|
|
1704
1966
|
}
|
|
1705
|
-
if (
|
|
1706
|
-
|
|
1967
|
+
if (fallback !== undefined) {
|
|
1968
|
+
if (typeof fallback === 'string'
|
|
1969
|
+
? header.alg !== fallback
|
|
1970
|
+
: typeof fallback === 'symbol'
|
|
1971
|
+
? !supported(header.alg)
|
|
1972
|
+
: !fallback.includes(header.alg)) {
|
|
1973
|
+
throw OPE('unexpected JWT "alg" header parameter', INVALID_RESPONSE, {
|
|
1974
|
+
header,
|
|
1975
|
+
expected: fallback,
|
|
1976
|
+
reason: 'default value',
|
|
1977
|
+
});
|
|
1978
|
+
}
|
|
1979
|
+
return;
|
|
1707
1980
|
}
|
|
1981
|
+
throw OPE('missing client or server configuration to verify used JWT "alg" header parameter', undefined, { client, issuer, fallback });
|
|
1708
1982
|
}
|
|
1709
1983
|
function getURLSearchParameter(parameters, name) {
|
|
1710
1984
|
const { 0: value, length } = parameters.getAll(name);
|
|
1711
1985
|
if (length > 1) {
|
|
1712
|
-
throw
|
|
1986
|
+
throw OPE(`"${name}" parameter must be provided only once`, INVALID_RESPONSE);
|
|
1713
1987
|
}
|
|
1714
1988
|
return value;
|
|
1715
1989
|
}
|
|
@@ -1722,46 +1996,47 @@ export function validateAuthResponse(as, client, parameters, expectedState) {
|
|
|
1722
1996
|
parameters = parameters.searchParams;
|
|
1723
1997
|
}
|
|
1724
1998
|
if (!(parameters instanceof URLSearchParams)) {
|
|
1725
|
-
throw
|
|
1999
|
+
throw CodedTypeError('"parameters" must be an instance of URLSearchParams, or URL', ERR_INVALID_ARG_TYPE);
|
|
1726
2000
|
}
|
|
1727
2001
|
if (getURLSearchParameter(parameters, 'response')) {
|
|
1728
|
-
throw
|
|
2002
|
+
throw OPE('"parameters" contains a JARM response, use validateJwtAuthResponse() instead of validateAuthResponse()', INVALID_RESPONSE, { parameters });
|
|
1729
2003
|
}
|
|
1730
2004
|
const iss = getURLSearchParameter(parameters, 'iss');
|
|
1731
2005
|
const state = getURLSearchParameter(parameters, 'state');
|
|
1732
2006
|
if (!iss && as.authorization_response_iss_parameter_supported) {
|
|
1733
|
-
throw
|
|
2007
|
+
throw OPE('response parameter "iss" (issuer) missing', INVALID_RESPONSE, { parameters });
|
|
1734
2008
|
}
|
|
1735
2009
|
if (iss && iss !== as.issuer) {
|
|
1736
|
-
throw
|
|
2010
|
+
throw OPE('unexpected "iss" (issuer) response parameter value', INVALID_RESPONSE, {
|
|
2011
|
+
expected: as.issuer,
|
|
2012
|
+
parameters,
|
|
2013
|
+
});
|
|
1737
2014
|
}
|
|
1738
2015
|
switch (expectedState) {
|
|
1739
2016
|
case undefined:
|
|
1740
2017
|
case expectNoState:
|
|
1741
2018
|
if (state !== undefined) {
|
|
1742
|
-
throw
|
|
2019
|
+
throw OPE('unexpected "state" response parameter encountered', INVALID_RESPONSE, {
|
|
2020
|
+
expected: undefined,
|
|
2021
|
+
parameters,
|
|
2022
|
+
});
|
|
1743
2023
|
}
|
|
1744
2024
|
break;
|
|
1745
2025
|
case skipStateCheck:
|
|
1746
2026
|
break;
|
|
1747
2027
|
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
|
-
}
|
|
2028
|
+
assertString(expectedState, '"expectedState" argument');
|
|
1754
2029
|
if (state !== expectedState) {
|
|
1755
|
-
throw
|
|
2030
|
+
throw OPE(state === undefined
|
|
2031
|
+
? 'response parameter "state" missing'
|
|
2032
|
+
: 'unexpected "state" response parameter value', INVALID_RESPONSE, { expected: expectedState, parameters });
|
|
1756
2033
|
}
|
|
1757
2034
|
}
|
|
1758
2035
|
const error = getURLSearchParameter(parameters, 'error');
|
|
1759
2036
|
if (error) {
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
error_uri: getURLSearchParameter(parameters, 'error_uri'),
|
|
1764
|
-
};
|
|
2037
|
+
throw new AuthorizationResponseError('authorization response from the server is an error', {
|
|
2038
|
+
cause: parameters,
|
|
2039
|
+
});
|
|
1765
2040
|
}
|
|
1766
2041
|
const id_token = getURLSearchParameter(parameters, 'id_token');
|
|
1767
2042
|
const token = getURLSearchParameter(parameters, 'token');
|
|
@@ -1770,7 +2045,7 @@ export function validateAuthResponse(as, client, parameters, expectedState) {
|
|
|
1770
2045
|
}
|
|
1771
2046
|
return brand(new URLSearchParams(parameters));
|
|
1772
2047
|
}
|
|
1773
|
-
function algToSubtle(alg
|
|
2048
|
+
function algToSubtle(alg) {
|
|
1774
2049
|
switch (alg) {
|
|
1775
2050
|
case 'PS256':
|
|
1776
2051
|
case 'PS384':
|
|
@@ -1785,96 +2060,94 @@ function algToSubtle(alg, crv) {
|
|
|
1785
2060
|
return { name: 'ECDSA', namedCurve: `P-${alg.slice(-3)}` };
|
|
1786
2061
|
case 'ES512':
|
|
1787
2062
|
return { name: 'ECDSA', namedCurve: 'P-521' };
|
|
1788
|
-
case '
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
case 'Ed448':
|
|
1792
|
-
return crv;
|
|
1793
|
-
default:
|
|
1794
|
-
throw new UnsupportedOperationError();
|
|
1795
|
-
}
|
|
1796
|
-
}
|
|
2063
|
+
case 'Ed25519':
|
|
2064
|
+
case 'EdDSA':
|
|
2065
|
+
return 'Ed25519';
|
|
1797
2066
|
default:
|
|
1798
|
-
throw new UnsupportedOperationError();
|
|
2067
|
+
throw new UnsupportedOperationError('unsupported JWS algorithm', { cause: { alg } });
|
|
1799
2068
|
}
|
|
1800
2069
|
}
|
|
1801
2070
|
async function importJwk(alg, jwk) {
|
|
1802
2071
|
const { ext, key_ops, use, ...key } = jwk;
|
|
1803
|
-
return crypto.subtle.importKey('jwk', key, algToSubtle(alg
|
|
2072
|
+
return crypto.subtle.importKey('jwk', key, algToSubtle(alg), true, ['verify']);
|
|
1804
2073
|
}
|
|
1805
|
-
export async function deviceAuthorizationRequest(as, client, parameters, options) {
|
|
2074
|
+
export async function deviceAuthorizationRequest(as, client, clientAuthentication, parameters, options) {
|
|
1806
2075
|
assertAs(as);
|
|
1807
2076
|
assertClient(client);
|
|
1808
|
-
const url = resolveEndpoint(as, 'device_authorization_endpoint',
|
|
2077
|
+
const url = resolveEndpoint(as, 'device_authorization_endpoint', client.use_mtls_endpoint_aliases, options?.[allowInsecureRequests] !== true);
|
|
1809
2078
|
const body = new URLSearchParams(parameters);
|
|
1810
2079
|
body.set('client_id', client.client_id);
|
|
1811
2080
|
const headers = prepareHeaders(options?.headers);
|
|
1812
2081
|
headers.set('accept', 'application/json');
|
|
1813
|
-
return authenticatedRequest(as, client,
|
|
2082
|
+
return authenticatedRequest(as, client, clientAuthentication, url, body, headers, options);
|
|
1814
2083
|
}
|
|
1815
2084
|
export async function processDeviceAuthorizationResponse(as, client, response) {
|
|
1816
2085
|
assertAs(as);
|
|
1817
2086
|
assertClient(client);
|
|
1818
2087
|
if (!looseInstanceOf(response, Response)) {
|
|
1819
|
-
throw
|
|
2088
|
+
throw CodedTypeError('"response" must be an instance of Response', ERR_INVALID_ARG_TYPE);
|
|
2089
|
+
}
|
|
2090
|
+
let challenges;
|
|
2091
|
+
if ((challenges = parseWwwAuthenticateChallenges(response))) {
|
|
2092
|
+
throw new WWWAuthenticateChallengeError('server responded with a challenge in the WWW-Authenticate HTTP Header', { cause: challenges, response });
|
|
1820
2093
|
}
|
|
1821
2094
|
if (response.status !== 200) {
|
|
1822
2095
|
let err;
|
|
1823
2096
|
if ((err = await handleOAuthBodyError(response))) {
|
|
1824
|
-
|
|
2097
|
+
await response.body?.cancel();
|
|
2098
|
+
throw new ResponseBodyError('server responded with an error in the response body', {
|
|
2099
|
+
cause: err,
|
|
2100
|
+
response,
|
|
2101
|
+
});
|
|
1825
2102
|
}
|
|
1826
|
-
throw
|
|
2103
|
+
throw OPE('"response" is not a conform Device Authorization Endpoint response', RESPONSE_IS_NOT_CONFORM, response);
|
|
1827
2104
|
}
|
|
1828
2105
|
assertReadableResponse(response);
|
|
2106
|
+
assertApplicationJson(response);
|
|
1829
2107
|
let json;
|
|
1830
2108
|
try {
|
|
1831
2109
|
json = await response.json();
|
|
1832
2110
|
}
|
|
1833
2111
|
catch (cause) {
|
|
1834
|
-
throw
|
|
2112
|
+
throw OPE('failed to parse "response" body as JSON', PARSE_ERROR, cause);
|
|
1835
2113
|
}
|
|
1836
2114
|
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');
|
|
2115
|
+
throw OPE('"response" body must be a top level object', INVALID_RESPONSE, { body: json });
|
|
1844
2116
|
}
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
}
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
}
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
2117
|
+
assertString(json.device_code, '"response" body "device_code" property', INVALID_RESPONSE, {
|
|
2118
|
+
body: json,
|
|
2119
|
+
});
|
|
2120
|
+
assertString(json.user_code, '"response" body "user_code" property', INVALID_RESPONSE, {
|
|
2121
|
+
body: json,
|
|
2122
|
+
});
|
|
2123
|
+
assertString(json.verification_uri, '"response" body "verification_uri" property', INVALID_RESPONSE, { body: json });
|
|
2124
|
+
assertNumber(json.expires_in, false, '"response" body "expires_in" property', INVALID_RESPONSE, {
|
|
2125
|
+
body: json,
|
|
2126
|
+
});
|
|
2127
|
+
if (json.verification_uri_complete !== undefined) {
|
|
2128
|
+
assertString(json.verification_uri_complete, '"response" body "verification_uri_complete" property', INVALID_RESPONSE, { body: json });
|
|
1854
2129
|
}
|
|
1855
|
-
if (json.interval !== undefined
|
|
1856
|
-
|
|
2130
|
+
if (json.interval !== undefined) {
|
|
2131
|
+
assertNumber(json.interval, false, '"response" body "interval" property', INVALID_RESPONSE, {
|
|
2132
|
+
body: json,
|
|
2133
|
+
});
|
|
1857
2134
|
}
|
|
1858
2135
|
return json;
|
|
1859
2136
|
}
|
|
1860
|
-
export async function deviceCodeGrantRequest(as, client, deviceCode, options) {
|
|
2137
|
+
export async function deviceCodeGrantRequest(as, client, clientAuthentication, deviceCode, options) {
|
|
1861
2138
|
assertAs(as);
|
|
1862
2139
|
assertClient(client);
|
|
1863
|
-
|
|
1864
|
-
throw new TypeError('"deviceCode" must be a non-empty string');
|
|
1865
|
-
}
|
|
2140
|
+
assertString(deviceCode, '"deviceCode"');
|
|
1866
2141
|
const parameters = new URLSearchParams(options?.additionalParameters);
|
|
1867
2142
|
parameters.set('device_code', deviceCode);
|
|
1868
|
-
return tokenEndpointRequest(as, client, 'urn:ietf:params:oauth:grant-type:device_code', parameters, options);
|
|
2143
|
+
return tokenEndpointRequest(as, client, clientAuthentication, 'urn:ietf:params:oauth:grant-type:device_code', parameters, options);
|
|
1869
2144
|
}
|
|
1870
|
-
export async function processDeviceCodeResponse(as, client, response) {
|
|
1871
|
-
return processGenericAccessTokenResponse(as, client, response);
|
|
2145
|
+
export async function processDeviceCodeResponse(as, client, response, options) {
|
|
2146
|
+
return processGenericAccessTokenResponse(as, client, response, undefined, options);
|
|
1872
2147
|
}
|
|
1873
2148
|
export async function generateKeyPair(alg, options) {
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
}
|
|
1877
|
-
const algorithm = algToSubtle(alg, alg === 'EdDSA' ? (options?.crv ?? 'Ed25519') : undefined);
|
|
2149
|
+
assertString(alg, '"alg"');
|
|
2150
|
+
const algorithm = algToSubtle(alg);
|
|
1878
2151
|
if (alg.startsWith('PS') || alg.startsWith('RS')) {
|
|
1879
2152
|
Object.assign(algorithm, {
|
|
1880
2153
|
modulusLength: options?.modulusLength ?? 2048,
|
|
@@ -1892,46 +2165,53 @@ function normalizeHtu(htu) {
|
|
|
1892
2165
|
url.hash = '';
|
|
1893
2166
|
return url.href;
|
|
1894
2167
|
}
|
|
1895
|
-
async function validateDPoP(
|
|
1896
|
-
const
|
|
1897
|
-
if (
|
|
1898
|
-
throw
|
|
2168
|
+
async function validateDPoP(request, accessToken, accessTokenClaims, options) {
|
|
2169
|
+
const headerValue = request.headers.get('dpop');
|
|
2170
|
+
if (headerValue === null) {
|
|
2171
|
+
throw OPE('operation indicated DPoP use but the request has no DPoP HTTP Header', INVALID_REQUEST, { headers: request.headers });
|
|
1899
2172
|
}
|
|
1900
2173
|
if (request.headers.get('authorization')?.toLowerCase().startsWith('dpop ') === false) {
|
|
1901
|
-
throw
|
|
2174
|
+
throw OPE(`operation indicated DPoP use but the request's Authorization HTTP Header scheme is not DPoP`, INVALID_REQUEST, { headers: request.headers });
|
|
1902
2175
|
}
|
|
1903
2176
|
if (typeof accessTokenClaims.cnf?.jkt !== 'string') {
|
|
1904
|
-
throw
|
|
2177
|
+
throw OPE('operation indicated DPoP use but the JWT Access Token has no jkt confirmation claim', INVALID_REQUEST, { claims: accessTokenClaims });
|
|
1905
2178
|
}
|
|
1906
2179
|
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)
|
|
2180
|
+
const proof = await validateJwt(headerValue, checkSigningAlgorithm.bind(undefined, options?.signingAlgorithms, undefined, SUPPORTED_JWS_ALGS), clockSkew, getClockTolerance(options), undefined)
|
|
1917
2181
|
.then(checkJwtType.bind(undefined, 'dpop+jwt'))
|
|
1918
2182
|
.then(validatePresence.bind(undefined, ['iat', 'jti', 'ath', 'htm', 'htu']));
|
|
1919
2183
|
const now = epochTime() + clockSkew;
|
|
1920
2184
|
const diff = Math.abs(now - proof.claims.iat);
|
|
1921
2185
|
if (diff > 300) {
|
|
1922
|
-
throw
|
|
2186
|
+
throw OPE('DPoP Proof iat is not recent enough', JWT_TIMESTAMP_CHECK, {
|
|
2187
|
+
now,
|
|
2188
|
+
claims: proof.claims,
|
|
2189
|
+
claim: 'iat',
|
|
2190
|
+
});
|
|
1923
2191
|
}
|
|
1924
2192
|
if (proof.claims.htm !== request.method) {
|
|
1925
|
-
throw
|
|
2193
|
+
throw OPE('DPoP Proof htm mismatch', JWT_CLAIM_COMPARISON, {
|
|
2194
|
+
expected: request.method,
|
|
2195
|
+
claims: proof.claims,
|
|
2196
|
+
claim: 'htm',
|
|
2197
|
+
});
|
|
1926
2198
|
}
|
|
1927
2199
|
if (typeof proof.claims.htu !== 'string' ||
|
|
1928
2200
|
normalizeHtu(proof.claims.htu) !== normalizeHtu(request.url)) {
|
|
1929
|
-
throw
|
|
2201
|
+
throw OPE('DPoP Proof htu mismatch', JWT_CLAIM_COMPARISON, {
|
|
2202
|
+
expected: normalizeHtu(request.url),
|
|
2203
|
+
claims: proof.claims,
|
|
2204
|
+
claim: 'htu',
|
|
2205
|
+
});
|
|
1930
2206
|
}
|
|
1931
2207
|
{
|
|
1932
|
-
const expected = b64u(await crypto.subtle.digest('SHA-256',
|
|
2208
|
+
const expected = b64u(await crypto.subtle.digest('SHA-256', buf(accessToken)));
|
|
1933
2209
|
if (proof.claims.ath !== expected) {
|
|
1934
|
-
throw
|
|
2210
|
+
throw OPE('DPoP Proof ath mismatch', JWT_CLAIM_COMPARISON, {
|
|
2211
|
+
expected,
|
|
2212
|
+
claims: proof.claims,
|
|
2213
|
+
claim: 'ath',
|
|
2214
|
+
});
|
|
1935
2215
|
}
|
|
1936
2216
|
}
|
|
1937
2217
|
{
|
|
@@ -1960,25 +2240,44 @@ async function validateDPoP(as, request, accessToken, accessTokenClaims, options
|
|
|
1960
2240
|
};
|
|
1961
2241
|
break;
|
|
1962
2242
|
default:
|
|
1963
|
-
throw new UnsupportedOperationError();
|
|
2243
|
+
throw new UnsupportedOperationError('unsupported JWK key type', { cause: proof.header.jwk });
|
|
1964
2244
|
}
|
|
1965
|
-
const expected = b64u(await crypto.subtle.digest('SHA-256',
|
|
2245
|
+
const expected = b64u(await crypto.subtle.digest('SHA-256', buf(JSON.stringify(components))));
|
|
1966
2246
|
if (accessTokenClaims.cnf.jkt !== expected) {
|
|
1967
|
-
throw
|
|
2247
|
+
throw OPE('JWT Access Token confirmation mismatch', JWT_CLAIM_COMPARISON, {
|
|
2248
|
+
expected,
|
|
2249
|
+
claims: accessTokenClaims,
|
|
2250
|
+
claim: 'cnf.jkt',
|
|
2251
|
+
});
|
|
1968
2252
|
}
|
|
1969
2253
|
}
|
|
2254
|
+
const { 0: protectedHeader, 1: payload, 2: encodedSignature } = headerValue.split('.');
|
|
2255
|
+
const signature = b64u(encodedSignature);
|
|
2256
|
+
const { jwk, alg } = proof.header;
|
|
2257
|
+
if (!jwk) {
|
|
2258
|
+
throw OPE('DPoP Proof is missing the jwk header parameter', INVALID_REQUEST, {
|
|
2259
|
+
header: proof.header,
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
const key = await importJwk(alg, jwk);
|
|
2263
|
+
if (key.type !== 'public') {
|
|
2264
|
+
throw OPE('DPoP Proof jwk header parameter must contain a public key', INVALID_REQUEST, {
|
|
2265
|
+
header: proof.header,
|
|
2266
|
+
});
|
|
2267
|
+
}
|
|
2268
|
+
await validateJwsSignature(protectedHeader, payload, key, signature);
|
|
1970
2269
|
}
|
|
1971
2270
|
export async function validateJwtAccessToken(as, request, expectedAudience, options) {
|
|
1972
2271
|
assertAs(as);
|
|
1973
2272
|
if (!looseInstanceOf(request, Request)) {
|
|
1974
|
-
throw
|
|
1975
|
-
}
|
|
1976
|
-
if (!validateString(expectedAudience)) {
|
|
1977
|
-
throw new OPE('"expectedAudience" must be a non-empty string');
|
|
2273
|
+
throw CodedTypeError('"request" must be an instance of Request', ERR_INVALID_ARG_TYPE);
|
|
1978
2274
|
}
|
|
2275
|
+
assertString(expectedAudience, '"expectedAudience"');
|
|
1979
2276
|
const authorization = request.headers.get('authorization');
|
|
1980
2277
|
if (authorization === null) {
|
|
1981
|
-
throw
|
|
2278
|
+
throw OPE('"request" is missing an Authorization HTTP Header', INVALID_REQUEST, {
|
|
2279
|
+
headers: request.headers,
|
|
2280
|
+
});
|
|
1982
2281
|
}
|
|
1983
2282
|
let { 0: scheme, 1: accessToken, length } = authorization.split(' ');
|
|
1984
2283
|
scheme = scheme.toLowerCase();
|
|
@@ -1987,10 +2286,14 @@ export async function validateJwtAccessToken(as, request, expectedAudience, opti
|
|
|
1987
2286
|
case 'bearer':
|
|
1988
2287
|
break;
|
|
1989
2288
|
default:
|
|
1990
|
-
throw new UnsupportedOperationError('unsupported Authorization HTTP Header scheme'
|
|
2289
|
+
throw new UnsupportedOperationError('unsupported Authorization HTTP Header scheme', {
|
|
2290
|
+
cause: { headers: request.headers },
|
|
2291
|
+
});
|
|
1991
2292
|
}
|
|
1992
2293
|
if (length !== 2) {
|
|
1993
|
-
throw
|
|
2294
|
+
throw OPE('invalid Authorization HTTP Header format', INVALID_REQUEST, {
|
|
2295
|
+
headers: request.headers,
|
|
2296
|
+
});
|
|
1994
2297
|
}
|
|
1995
2298
|
const requiredClaims = [
|
|
1996
2299
|
'iss',
|
|
@@ -2004,43 +2307,54 @@ export async function validateJwtAccessToken(as, request, expectedAudience, opti
|
|
|
2004
2307
|
if (options?.requireDPoP || scheme === 'dpop' || request.headers.has('dpop')) {
|
|
2005
2308
|
requiredClaims.push('cnf');
|
|
2006
2309
|
}
|
|
2007
|
-
const { claims } = await validateJwt(accessToken, checkSigningAlgorithm.bind(undefined,
|
|
2310
|
+
const { claims, header } = await validateJwt(accessToken, checkSigningAlgorithm.bind(undefined, options?.signingAlgorithms, undefined, SUPPORTED_JWS_ALGS), getClockSkew(options), getClockTolerance(options), undefined)
|
|
2008
2311
|
.then(checkJwtType.bind(undefined, 'at+jwt'))
|
|
2009
2312
|
.then(validatePresence.bind(undefined, requiredClaims))
|
|
2010
|
-
.then(validateIssuer.bind(undefined, as
|
|
2011
|
-
.then(validateAudience.bind(undefined, expectedAudience))
|
|
2313
|
+
.then(validateIssuer.bind(undefined, as))
|
|
2314
|
+
.then(validateAudience.bind(undefined, expectedAudience))
|
|
2315
|
+
.catch(reassignRSCode);
|
|
2012
2316
|
for (const claim of ['client_id', 'jti', 'sub']) {
|
|
2013
2317
|
if (typeof claims[claim] !== 'string') {
|
|
2014
|
-
throw
|
|
2318
|
+
throw OPE(`unexpected JWT "${claim}" claim type`, INVALID_REQUEST, { claims });
|
|
2015
2319
|
}
|
|
2016
2320
|
}
|
|
2017
2321
|
if ('cnf' in claims) {
|
|
2018
2322
|
if (!isJsonObject(claims.cnf)) {
|
|
2019
|
-
throw
|
|
2323
|
+
throw OPE('unexpected JWT "cnf" (confirmation) claim value', INVALID_REQUEST, { claims });
|
|
2020
2324
|
}
|
|
2021
2325
|
const { 0: cnf, length } = Object.keys(claims.cnf);
|
|
2022
2326
|
if (length) {
|
|
2023
2327
|
if (length !== 1) {
|
|
2024
|
-
throw new UnsupportedOperationError('multiple confirmation claims are not supported'
|
|
2328
|
+
throw new UnsupportedOperationError('multiple confirmation claims are not supported', {
|
|
2329
|
+
cause: { claims },
|
|
2330
|
+
});
|
|
2025
2331
|
}
|
|
2026
2332
|
if (cnf !== 'jkt') {
|
|
2027
|
-
throw new UnsupportedOperationError('unsupported JWT Confirmation method'
|
|
2333
|
+
throw new UnsupportedOperationError('unsupported JWT Confirmation method', {
|
|
2334
|
+
cause: { claims },
|
|
2335
|
+
});
|
|
2028
2336
|
}
|
|
2029
2337
|
}
|
|
2030
2338
|
}
|
|
2339
|
+
const { 0: protectedHeader, 1: payload, 2: encodedSignature } = accessToken.split('.');
|
|
2340
|
+
const signature = b64u(encodedSignature);
|
|
2341
|
+
const key = await getPublicSigKeyFromIssuerJwksUri(as, options, header);
|
|
2342
|
+
await validateJwsSignature(protectedHeader, payload, key, signature);
|
|
2031
2343
|
if (options?.requireDPoP ||
|
|
2032
2344
|
scheme === 'dpop' ||
|
|
2033
2345
|
claims.cnf?.jkt !== undefined ||
|
|
2034
2346
|
request.headers.has('dpop')) {
|
|
2035
|
-
await validateDPoP(
|
|
2347
|
+
await validateDPoP(request, accessToken, claims, options).catch(reassignRSCode);
|
|
2036
2348
|
}
|
|
2037
2349
|
return claims;
|
|
2038
2350
|
}
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
export const
|
|
2046
|
-
export const
|
|
2351
|
+
function reassignRSCode(err) {
|
|
2352
|
+
if (err instanceof OperationProcessingError && err?.code === INVALID_REQUEST) {
|
|
2353
|
+
err.code = INVALID_RESPONSE;
|
|
2354
|
+
}
|
|
2355
|
+
throw err;
|
|
2356
|
+
}
|
|
2357
|
+
export const _nopkce = Symbol();
|
|
2358
|
+
export const _nodiscoverycheck = Symbol();
|
|
2359
|
+
export const _expectedIssuer = Symbol();
|
|
2360
|
+
//# sourceMappingURL=index.js.map
|