oauth4webapi 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.js ADDED
@@ -0,0 +1,1589 @@
1
+ let USER_AGENT;
2
+ if (typeof navigator === 'undefined' || !navigator.userAgent?.startsWith?.('Mozilla/5.0 ')) {
3
+ const NAME = 'oauth4webapi';
4
+ const VERSION = 'v1.2.1';
5
+ USER_AGENT = `${NAME}/${VERSION}`;
6
+ }
7
+ const encoder = new TextEncoder();
8
+ const decoder = new TextDecoder();
9
+ function buf(input) {
10
+ if (typeof input === 'string') {
11
+ return encoder.encode(input);
12
+ }
13
+ return decoder.decode(input);
14
+ }
15
+ const CHUNK_SIZE = 0x8000;
16
+ function encodeBase64Url(input) {
17
+ if (input instanceof ArrayBuffer) {
18
+ input = new Uint8Array(input);
19
+ }
20
+ const arr = [];
21
+ for (let i = 0; i < input.byteLength; i += CHUNK_SIZE) {
22
+ arr.push(String.fromCharCode.apply(null, input.subarray(i, i + CHUNK_SIZE)));
23
+ }
24
+ return btoa(arr.join('')).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
25
+ }
26
+ function decodeBase64Url(input) {
27
+ try {
28
+ const binary = atob(input.replace(/-/g, '+').replace(/_/g, '/').replace(/\s/g, ''));
29
+ const bytes = new Uint8Array(binary.length);
30
+ for (let i = 0; i < binary.length; i++) {
31
+ bytes[i] = binary.charCodeAt(i);
32
+ }
33
+ return bytes;
34
+ }
35
+ catch {
36
+ throw new TypeError('The input to be decoded is not correctly encoded.');
37
+ }
38
+ }
39
+ function b64u(input) {
40
+ if (typeof input === 'string') {
41
+ return decodeBase64Url(input);
42
+ }
43
+ return encodeBase64Url(input);
44
+ }
45
+ class LRU {
46
+ constructor(maxSize) {
47
+ this.cache = new Map();
48
+ this._cache = new Map();
49
+ this.maxSize = maxSize;
50
+ }
51
+ get(key) {
52
+ let v = this.cache.get(key);
53
+ if (v) {
54
+ return v;
55
+ }
56
+ if ((v = this._cache.get(key))) {
57
+ this.update(key, v);
58
+ return v;
59
+ }
60
+ return undefined;
61
+ }
62
+ has(key) {
63
+ return this.cache.has(key) || this._cache.has(key);
64
+ }
65
+ set(key, value) {
66
+ if (this.cache.has(key)) {
67
+ this.cache.set(key, value);
68
+ }
69
+ else {
70
+ this.update(key, value);
71
+ }
72
+ return this;
73
+ }
74
+ delete(key) {
75
+ if (this.cache.has(key)) {
76
+ return this.cache.delete(key);
77
+ }
78
+ if (this._cache.has(key)) {
79
+ return this._cache.delete(key);
80
+ }
81
+ return false;
82
+ }
83
+ update(key, value) {
84
+ this.cache.set(key, value);
85
+ if (this.cache.size >= this.maxSize) {
86
+ this._cache = this.cache;
87
+ this.cache = new Map();
88
+ }
89
+ }
90
+ }
91
+ export class UnsupportedOperationError extends Error {
92
+ constructor(message) {
93
+ super(message ?? 'operation not supported');
94
+ this.name = this.constructor.name;
95
+ Error.captureStackTrace?.(this, this.constructor);
96
+ }
97
+ }
98
+ export class OperationProcessingError extends Error {
99
+ constructor(message) {
100
+ super(message);
101
+ this.name = this.constructor.name;
102
+ Error.captureStackTrace?.(this, this.constructor);
103
+ }
104
+ }
105
+ const OPE = OperationProcessingError;
106
+ const dpopNonces = new LRU(100);
107
+ function isCryptoKey(key) {
108
+ return key instanceof CryptoKey;
109
+ }
110
+ function isPrivateKey(key) {
111
+ return isCryptoKey(key) && key.type === 'private';
112
+ }
113
+ function isPublicKey(key) {
114
+ return isCryptoKey(key) && key.type === 'public';
115
+ }
116
+ const SUPPORTED_JWS_ALGS = ['PS256', 'ES256', 'RS256', 'EdDSA'];
117
+ function preserveBodyStream(response) {
118
+ assertReadableResponse(response);
119
+ return response.clone();
120
+ }
121
+ function processDpopNonce(response) {
122
+ const url = new URL(response.url);
123
+ if (response.headers.has('dpop-nonce')) {
124
+ dpopNonces.set(url.origin, response.headers.get('dpop-nonce'));
125
+ }
126
+ return response;
127
+ }
128
+ function normalizeTyp(value) {
129
+ return value.toLowerCase().replace(/^application\//, '');
130
+ }
131
+ function isJsonObject(input) {
132
+ if (input === null || typeof input !== 'object' || Array.isArray(input)) {
133
+ return false;
134
+ }
135
+ return true;
136
+ }
137
+ function prepareHeaders(input) {
138
+ if (input !== undefined && !(input instanceof Headers)) {
139
+ throw new TypeError('"options.headers" must be an instance of Headers');
140
+ }
141
+ const headers = new Headers(input);
142
+ if (USER_AGENT && !headers.has('user-agent')) {
143
+ headers.set('user-agent', USER_AGENT);
144
+ }
145
+ if (headers.has('authorization')) {
146
+ throw new TypeError('"options.headers" must not include the "authorization" header name');
147
+ }
148
+ if (headers.has('dpop')) {
149
+ throw new TypeError('"options.headers" must not include the "dpop" header name');
150
+ }
151
+ return headers;
152
+ }
153
+ function signal(value) {
154
+ return typeof value === 'function' ? value() : value;
155
+ }
156
+ export async function discoveryRequest(issuerIdentifier, options) {
157
+ if (!(issuerIdentifier instanceof URL)) {
158
+ throw new TypeError('"issuer" must be an instance of URL');
159
+ }
160
+ if (issuerIdentifier.protocol !== 'https:' && issuerIdentifier.protocol !== 'http:') {
161
+ throw new TypeError('"issuer.protocol" must be "https:" or "http:"');
162
+ }
163
+ const url = new URL(issuerIdentifier.href);
164
+ switch (options?.algorithm) {
165
+ case undefined:
166
+ case 'oidc':
167
+ url.pathname = `${url.pathname}/.well-known/openid-configuration`.replace('//', '/');
168
+ break;
169
+ case 'oauth2':
170
+ if (url.pathname === '/') {
171
+ url.pathname = `.well-known/oauth-authorization-server`;
172
+ }
173
+ else {
174
+ url.pathname = `.well-known/oauth-authorization-server/${url.pathname}`.replace('//', '/');
175
+ }
176
+ break;
177
+ default:
178
+ throw new TypeError('"options.algorithm" must be "oidc" (default), or "oauth2"');
179
+ }
180
+ const headers = prepareHeaders(options?.headers);
181
+ headers.set('accept', 'application/json');
182
+ return fetch(url.href, {
183
+ headers,
184
+ method: 'GET',
185
+ redirect: 'manual',
186
+ signal: options?.signal ? signal(options.signal) : null,
187
+ }).then(processDpopNonce);
188
+ }
189
+ function validateString(input) {
190
+ return typeof input === 'string' && input.length !== 0;
191
+ }
192
+ export async function processDiscoveryResponse(expectedIssuerIdentifier, response) {
193
+ if (!(expectedIssuerIdentifier instanceof URL)) {
194
+ throw new TypeError('"expectedIssuer" must be an instance of URL');
195
+ }
196
+ if (!(response instanceof Response)) {
197
+ throw new TypeError('"response" must be an instance of Response');
198
+ }
199
+ if (response.status !== 200) {
200
+ throw new OPE('"response" is not a conform Authorization Server Metadata response');
201
+ }
202
+ let json;
203
+ try {
204
+ json = await preserveBodyStream(response).json();
205
+ }
206
+ catch {
207
+ throw new OPE('failed to parse "response" body as JSON');
208
+ }
209
+ if (!isJsonObject(json)) {
210
+ throw new OPE('"response" body must be a top level object');
211
+ }
212
+ if (!validateString(json.issuer)) {
213
+ throw new OPE('"response" body "issuer" property must be a non-empty string');
214
+ }
215
+ if (new URL(json.issuer).href !== expectedIssuerIdentifier.href) {
216
+ throw new OPE('"response" body "issuer" does not match "expectedIssuer"');
217
+ }
218
+ return json;
219
+ }
220
+ function randomBytes() {
221
+ return b64u(crypto.getRandomValues(new Uint8Array(32)));
222
+ }
223
+ export function generateRandomCodeVerifier() {
224
+ return randomBytes();
225
+ }
226
+ export function generateRandomState() {
227
+ return randomBytes();
228
+ }
229
+ export function generateRandomNonce() {
230
+ return randomBytes();
231
+ }
232
+ export async function calculatePKCECodeChallenge(codeVerifier) {
233
+ if (!validateString(codeVerifier)) {
234
+ throw new TypeError('"codeVerifier" must be a non-empty string');
235
+ }
236
+ return b64u(await crypto.subtle.digest({ name: 'SHA-256' }, buf(codeVerifier)));
237
+ }
238
+ function getKeyAndKid(input) {
239
+ if (input instanceof CryptoKey) {
240
+ return { key: input };
241
+ }
242
+ if (!(input?.key instanceof CryptoKey)) {
243
+ return {};
244
+ }
245
+ if (input.kid !== undefined && !validateString(input.kid)) {
246
+ throw new TypeError('"kid" must be a non-empty string');
247
+ }
248
+ return { key: input.key, kid: input.kid };
249
+ }
250
+ function formUrlEncode(token) {
251
+ return encodeURIComponent(token).replace(/%20/g, '+');
252
+ }
253
+ function clientSecretBasic(clientId, clientSecret) {
254
+ const username = formUrlEncode(clientId);
255
+ const password = formUrlEncode(clientSecret);
256
+ const credentials = btoa(`${username}:${password}`);
257
+ return `Basic ${credentials}`;
258
+ }
259
+ function psAlg(key) {
260
+ switch (key.algorithm.hash.name) {
261
+ case 'SHA-256':
262
+ return 'PS256';
263
+ default:
264
+ throw new UnsupportedOperationError('unsupported RsaHashedKeyAlgorithm hash name');
265
+ }
266
+ }
267
+ function rsAlg(key) {
268
+ switch (key.algorithm.hash.name) {
269
+ case 'SHA-256':
270
+ return 'RS256';
271
+ default:
272
+ throw new UnsupportedOperationError('unsupported RsaHashedKeyAlgorithm hash name');
273
+ }
274
+ }
275
+ function esAlg(key) {
276
+ switch (key.algorithm.namedCurve) {
277
+ case 'P-256':
278
+ return 'ES256';
279
+ default:
280
+ throw new UnsupportedOperationError('unsupported EcKeyAlgorithm namedCurve');
281
+ }
282
+ }
283
+ function determineJWSAlgorithm(key) {
284
+ switch (key.algorithm.name) {
285
+ case 'RSA-PSS':
286
+ return psAlg(key);
287
+ case 'RSASSA-PKCS1-v1_5':
288
+ return rsAlg(key);
289
+ case 'ECDSA':
290
+ return esAlg(key);
291
+ case 'Ed25519':
292
+ return 'EdDSA';
293
+ default:
294
+ throw new UnsupportedOperationError('unsupported CryptoKey algorithm name');
295
+ }
296
+ }
297
+ function epochTime() {
298
+ return Math.floor(Date.now() / 1000);
299
+ }
300
+ function clientAssertion(as, client) {
301
+ const now = epochTime();
302
+ return {
303
+ jti: randomBytes(),
304
+ aud: [as.issuer, as.token_endpoint],
305
+ exp: now + 60,
306
+ iat: now,
307
+ nbf: now,
308
+ iss: client.client_id,
309
+ sub: client.client_id,
310
+ };
311
+ }
312
+ async function privateKeyJwt(as, client, key, kid) {
313
+ return jwt({
314
+ alg: determineJWSAlgorithm(key),
315
+ kid,
316
+ }, clientAssertion(as, client), key);
317
+ }
318
+ function assertIssuer(metadata) {
319
+ if (typeof metadata !== 'object' || metadata === null) {
320
+ throw new TypeError('"issuer" must be an object');
321
+ }
322
+ if (!validateString(metadata.issuer)) {
323
+ throw new TypeError('"issuer.issuer" property must be a non-empty string');
324
+ }
325
+ return true;
326
+ }
327
+ function assertClient(metadata) {
328
+ if (typeof metadata !== 'object' || metadata === null) {
329
+ throw new TypeError('"client" must be an object');
330
+ }
331
+ if (!validateString(metadata.client_id)) {
332
+ throw new TypeError('"client.client_id" property must be a non-empty string');
333
+ }
334
+ return true;
335
+ }
336
+ function assertClientSecret(clientSecret) {
337
+ if (!validateString(clientSecret)) {
338
+ throw new TypeError('"client.client_secret" property must be a non-empty string');
339
+ }
340
+ return clientSecret;
341
+ }
342
+ function assertNoClientPrivateKey(clientAuthMethod, clientPrivateKey) {
343
+ if (clientPrivateKey !== undefined) {
344
+ throw new TypeError(`"options.clientPrivateKey" property must not be provided when ${clientAuthMethod} client authentication method is used.`);
345
+ }
346
+ }
347
+ function assertNoClientSecret(clientAuthMethod, clientSecret) {
348
+ if (clientSecret !== undefined) {
349
+ throw new TypeError(`"client.client_secret" property must not be provided when ${clientAuthMethod} client authentication method is used.`);
350
+ }
351
+ }
352
+ async function clientAuthentication(as, client, body, headers, clientPrivateKey) {
353
+ body.delete('client_secret');
354
+ body.delete('client_assertion_type');
355
+ body.delete('client_assertion');
356
+ switch (client.token_endpoint_auth_method) {
357
+ case undefined:
358
+ case 'client_secret_basic': {
359
+ assertNoClientPrivateKey('client_secret_basic', clientPrivateKey);
360
+ headers.set('authorization', clientSecretBasic(client.client_id, assertClientSecret(client.client_secret)));
361
+ break;
362
+ }
363
+ case 'client_secret_post': {
364
+ assertNoClientPrivateKey('client_secret_post', clientPrivateKey);
365
+ body.set('client_id', client.client_id);
366
+ body.set('client_secret', assertClientSecret(client.client_secret));
367
+ break;
368
+ }
369
+ case 'private_key_jwt': {
370
+ assertNoClientSecret('private_key_jwt', client.client_secret);
371
+ if (clientPrivateKey === undefined) {
372
+ throw new TypeError('"options.clientPrivateKey" must be provided when "client.token_endpoint_auth_method" is "private_key_jwt"');
373
+ }
374
+ const { key, kid } = getKeyAndKid(clientPrivateKey);
375
+ if (!isPrivateKey(key)) {
376
+ throw new TypeError('"options.clientPrivateKey.key" must be a private CryptoKey');
377
+ }
378
+ body.set('client_id', client.client_id);
379
+ body.set('client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');
380
+ body.set('client_assertion', await privateKeyJwt(as, client, key, kid));
381
+ break;
382
+ }
383
+ case 'none': {
384
+ assertNoClientSecret('none', client.client_secret);
385
+ assertNoClientPrivateKey('none', clientPrivateKey);
386
+ body.set('client_id', client.client_id);
387
+ break;
388
+ }
389
+ default:
390
+ throw new UnsupportedOperationError('unsupported client token_endpoint_auth_method');
391
+ }
392
+ }
393
+ async function jwt(header, claimsSet, key) {
394
+ if (!key.usages.includes('sign')) {
395
+ throw new TypeError('CryptoKey instances used for signing assertions must include "sign" in their "usages"');
396
+ }
397
+ const input = `${b64u(buf(JSON.stringify(header)))}.${b64u(buf(JSON.stringify(claimsSet)))}`;
398
+ const signature = b64u(await crypto.subtle.sign(subtleAlgorithm(key), key, buf(input)));
399
+ return `${input}.${signature}`;
400
+ }
401
+ export async function issueRequestObject(as, client, parameters, privateKey) {
402
+ assertIssuer(as);
403
+ assertClient(client);
404
+ if (!(parameters instanceof URLSearchParams)) {
405
+ throw new TypeError('"parameters" must be an instance of URLSearchParams');
406
+ }
407
+ parameters = new URLSearchParams(parameters);
408
+ const { key, kid } = getKeyAndKid(privateKey);
409
+ if (!isPrivateKey(key)) {
410
+ throw new TypeError('"privateKey.key" must be a private CryptoKey');
411
+ }
412
+ parameters.set('client_id', client.client_id);
413
+ const now = epochTime();
414
+ const claims = {
415
+ ...Object.fromEntries(parameters.entries()),
416
+ jti: randomBytes(),
417
+ aud: as.issuer,
418
+ exp: now + 60,
419
+ iat: now,
420
+ nbf: now,
421
+ iss: client.client_id,
422
+ };
423
+ let resource;
424
+ if (parameters.has('resource') &&
425
+ (resource = parameters.getAll('resource')) &&
426
+ resource.length > 1) {
427
+ claims.resource = resource;
428
+ }
429
+ return jwt({
430
+ alg: determineJWSAlgorithm(key),
431
+ typ: 'oauth-authz-req+jwt',
432
+ kid,
433
+ }, claims, key);
434
+ }
435
+ async function dpopProofJwt(headers, options, url, htm, accessToken) {
436
+ const { privateKey, publicKey, nonce = dpopNonces.get(url.origin) } = options;
437
+ if (!isPrivateKey(privateKey)) {
438
+ throw new TypeError('"DPoP.privateKey" must be a private CryptoKey');
439
+ }
440
+ if (!isPublicKey(publicKey)) {
441
+ throw new TypeError('"DPoP.publicKey" must be a public CryptoKey');
442
+ }
443
+ if (nonce !== undefined && !validateString(nonce)) {
444
+ throw new TypeError('"DPoP.nonce" must be a non-empty string or undefined');
445
+ }
446
+ if (!publicKey.extractable) {
447
+ throw new TypeError('"DPoP.publicKey.extractable" must be true');
448
+ }
449
+ const now = epochTime();
450
+ const proof = await jwt({
451
+ alg: determineJWSAlgorithm(privateKey),
452
+ typ: 'dpop+jwt',
453
+ jwk: await publicJwk(publicKey),
454
+ }, {
455
+ iat: now,
456
+ jti: randomBytes(),
457
+ htm,
458
+ nonce,
459
+ htu: `${url.origin}${url.pathname}`,
460
+ ath: accessToken
461
+ ? b64u(await crypto.subtle.digest({ name: 'SHA-256' }, buf(accessToken)))
462
+ : undefined,
463
+ }, privateKey);
464
+ headers.set('dpop', proof);
465
+ }
466
+ async function publicJwk(key) {
467
+ const { kty, e, n, x, y, crv } = await crypto.subtle.exportKey('jwk', key);
468
+ return { kty, crv, e, n, x, y };
469
+ }
470
+ export async function pushedAuthorizationRequest(as, client, parameters, options) {
471
+ assertIssuer(as);
472
+ assertClient(client);
473
+ if (!(parameters instanceof URLSearchParams)) {
474
+ throw new TypeError('"parameters" must be an instance of URLSearchParams');
475
+ }
476
+ if (typeof as.pushed_authorization_request_endpoint !== 'string') {
477
+ throw new TypeError('"issuer.pushed_authorization_request_endpoint" must be a string');
478
+ }
479
+ const url = new URL(as.pushed_authorization_request_endpoint);
480
+ const body = new URLSearchParams(parameters);
481
+ body.set('client_id', client.client_id);
482
+ const headers = prepareHeaders(options?.headers);
483
+ headers.set('accept', 'application/json');
484
+ if (options?.DPoP !== undefined) {
485
+ await dpopProofJwt(headers, options.DPoP, url, 'POST');
486
+ if (!body.has('dpop_jkt')) {
487
+ body.set('dpop_jkt', await calculateJwkThumbprint(options.DPoP.publicKey));
488
+ }
489
+ }
490
+ return authenticatedRequest(as, client, 'POST', url, body, headers, options);
491
+ }
492
+ export function isOAuth2Error(input) {
493
+ const value = input;
494
+ if (typeof value !== 'object' || Array.isArray(value) || value === null) {
495
+ return false;
496
+ }
497
+ return value.error !== undefined;
498
+ }
499
+ function unquote(value) {
500
+ if (value.length >= 2 && value[0] === '"' && value[value.length - 1] === '"') {
501
+ return value.slice(1, -1);
502
+ }
503
+ return value;
504
+ }
505
+ const SPLIT_REGEXP = /((?:,|, )?[0-9a-zA-Z!#$%&'*+-.^_`|~]+=)/;
506
+ const SCHEMES_REGEXP = /(?:^|, ?)([0-9a-zA-Z!#$%&'*+\-.^_`|~]+)(?=$|[ ,])/g;
507
+ function wwwAuth(scheme, params) {
508
+ const arr = params.split(SPLIT_REGEXP).slice(1);
509
+ if (!arr.length) {
510
+ return { scheme: scheme.toLowerCase(), parameters: {} };
511
+ }
512
+ arr[arr.length - 1] = arr[arr.length - 1].replace(/,$/, '');
513
+ const parameters = {};
514
+ for (let i = 1; i < arr.length; i += 2) {
515
+ const idx = i;
516
+ if (arr[idx][0] === '"') {
517
+ while (arr[idx].slice(-1) !== '"' && ++i < arr.length) {
518
+ arr[idx] += arr[i];
519
+ }
520
+ }
521
+ const key = arr[idx - 1].replace(/^(?:, ?)|=$/g, '').toLowerCase();
522
+ parameters[key] = unquote(arr[idx]);
523
+ }
524
+ return {
525
+ scheme: scheme.toLowerCase(),
526
+ parameters,
527
+ };
528
+ }
529
+ export function parseWwwAuthenticateChallenges(response) {
530
+ if (!(response instanceof Response)) {
531
+ throw new TypeError('"response" must be an instance of Response');
532
+ }
533
+ if (!response.headers.has('www-authenticate')) {
534
+ return undefined;
535
+ }
536
+ const header = response.headers.get('www-authenticate');
537
+ const result = [];
538
+ for (const { 1: scheme, index } of header.matchAll(SCHEMES_REGEXP)) {
539
+ result.push([scheme, index]);
540
+ }
541
+ if (!result.length) {
542
+ return undefined;
543
+ }
544
+ const challenges = result.map(([scheme, indexOf], i, others) => {
545
+ const next = others[i + 1];
546
+ let parameters;
547
+ if (next) {
548
+ parameters = header.slice(indexOf, next[1]);
549
+ }
550
+ else {
551
+ parameters = header.slice(indexOf);
552
+ }
553
+ return wwwAuth(scheme, parameters);
554
+ });
555
+ return challenges;
556
+ }
557
+ export async function processPushedAuthorizationResponse(as, client, response) {
558
+ assertIssuer(as);
559
+ assertClient(client);
560
+ if (!(response instanceof Response)) {
561
+ throw new TypeError('"response" must be an instance of Response');
562
+ }
563
+ if (response.status !== 201) {
564
+ let err;
565
+ if ((err = await handleOAuthBodyError(response))) {
566
+ return err;
567
+ }
568
+ throw new OPE('"response" is not a conform Pushed Authorization Request Endpoint response');
569
+ }
570
+ let json;
571
+ try {
572
+ json = await preserveBodyStream(response).json();
573
+ }
574
+ catch {
575
+ throw new OPE('failed to parse "response" body as JSON');
576
+ }
577
+ if (!isJsonObject(json)) {
578
+ throw new OPE('"response" body must be a top level object');
579
+ }
580
+ if (!validateString(json.request_uri)) {
581
+ throw new OPE('"response" body "request_uri" property must be a non-empty string');
582
+ }
583
+ if (typeof json.expires_in !== 'number' || json.expires_in <= 0) {
584
+ throw new OPE('"response" body "expires_in" property must be a positive number');
585
+ }
586
+ return json;
587
+ }
588
+ export async function protectedResourceRequest(accessToken, method, url, headers, body, options) {
589
+ if (!validateString(accessToken)) {
590
+ throw new TypeError('"accessToken" must be a non-empty string');
591
+ }
592
+ if (!(url instanceof URL)) {
593
+ throw new TypeError('"url" must be an instance of URL');
594
+ }
595
+ headers = prepareHeaders(headers);
596
+ if (options?.DPoP === undefined) {
597
+ headers.set('authorization', `Bearer ${accessToken}`);
598
+ }
599
+ else {
600
+ await dpopProofJwt(headers, options.DPoP, url, 'GET', accessToken);
601
+ headers.set('authorization', `DPoP ${accessToken}`);
602
+ }
603
+ return fetch(url.href, {
604
+ body,
605
+ headers,
606
+ method,
607
+ redirect: 'manual',
608
+ signal: options?.signal ? signal(options.signal) : null,
609
+ }).then(processDpopNonce);
610
+ }
611
+ export async function userInfoRequest(as, client, accessToken, options) {
612
+ assertIssuer(as);
613
+ assertClient(client);
614
+ if (typeof as.userinfo_endpoint !== 'string') {
615
+ throw new TypeError('"issuer.userinfo_endpoint" must be a string');
616
+ }
617
+ const url = new URL(as.userinfo_endpoint);
618
+ const headers = prepareHeaders(options?.headers);
619
+ if (client.userinfo_signed_response_alg) {
620
+ headers.set('accept', 'application/jwt');
621
+ }
622
+ else {
623
+ headers.set('accept', 'application/json');
624
+ headers.append('accept', 'application/jwt');
625
+ }
626
+ return protectedResourceRequest(accessToken, 'GET', url, headers, null, options);
627
+ }
628
+ const jwksCache = new LRU(20);
629
+ const cryptoKeyCaches = {};
630
+ async function getPublicSigKeyFromIssuerJwksUri(as, options, header) {
631
+ const { alg, kid } = header;
632
+ checkSupportedJwsAlg(alg);
633
+ let jwks;
634
+ let age;
635
+ if (jwksCache.has(as.jwks_uri)) {
636
+ ;
637
+ ({ jwks, age } = jwksCache.get(as.jwks_uri));
638
+ if (age >= 300) {
639
+ jwksCache.delete(as.jwks_uri);
640
+ return getPublicSigKeyFromIssuerJwksUri(as, options, header);
641
+ }
642
+ }
643
+ else {
644
+ jwks = await jwksRequest(as, options).then(processJwksResponse);
645
+ age = 0;
646
+ jwksCache.set(as.jwks_uri, {
647
+ jwks,
648
+ iat: epochTime(),
649
+ get age() {
650
+ return epochTime() - this.iat;
651
+ },
652
+ });
653
+ }
654
+ let kty;
655
+ switch (alg.slice(0, 2)) {
656
+ case 'RS':
657
+ case 'PS':
658
+ kty = 'RSA';
659
+ break;
660
+ case 'ES':
661
+ kty = 'EC';
662
+ break;
663
+ case 'Ed':
664
+ kty = 'OKP';
665
+ break;
666
+ default:
667
+ throw new UnsupportedOperationError();
668
+ }
669
+ const candidates = jwks.keys.filter((jwk) => {
670
+ if (jwk.kty !== kty) {
671
+ return false;
672
+ }
673
+ if (kid !== undefined && kid !== jwk.kid) {
674
+ return false;
675
+ }
676
+ if (jwk.alg !== undefined && alg !== jwk.alg) {
677
+ return false;
678
+ }
679
+ if (jwk.use !== undefined && jwk.use !== 'sig') {
680
+ return false;
681
+ }
682
+ if (jwk.key_ops?.includes('verify') === false) {
683
+ return false;
684
+ }
685
+ switch (true) {
686
+ case alg === 'ES256' && jwk.crv !== 'P-256':
687
+ case alg === 'EdDSA' && jwk.crv !== 'Ed25519':
688
+ return false;
689
+ }
690
+ return true;
691
+ });
692
+ const { 0: jwk, length } = candidates;
693
+ if (!length) {
694
+ if (age >= 60) {
695
+ jwksCache.delete(as.jwks_uri);
696
+ return getPublicSigKeyFromIssuerJwksUri(as, options, header);
697
+ }
698
+ throw new OPE('error when selecting a JWT verification key, no applicable keys found');
699
+ }
700
+ else if (length !== 1) {
701
+ throw new OPE('error when selecting a JWT verification key, multiple applicable keys found, a "kid" JWT Header Parameter is required');
702
+ }
703
+ cryptoKeyCaches[alg] || (cryptoKeyCaches[alg] = new WeakMap());
704
+ let key = cryptoKeyCaches[alg].get(jwk);
705
+ if (!key) {
706
+ key = await importJwk({ ...jwk, alg });
707
+ if (key.type !== 'public') {
708
+ throw new OPE('jwks_uri must only contain public keys');
709
+ }
710
+ cryptoKeyCaches[alg].set(jwk, key);
711
+ }
712
+ return key;
713
+ }
714
+ export const skipSubjectCheck = Symbol();
715
+ function getContentType(response) {
716
+ return response.headers.get('content-type')?.split(';')[0];
717
+ }
718
+ export async function processUserInfoResponse(as, client, expectedSubject, response, options) {
719
+ assertIssuer(as);
720
+ assertClient(client);
721
+ if (!(response instanceof Response)) {
722
+ throw new TypeError('"response" must be an instance of Response');
723
+ }
724
+ if (response.status !== 200) {
725
+ throw new OPE('"response" is not a conform UserInfo Endpoint response');
726
+ }
727
+ let json;
728
+ if (getContentType(response) === 'application/jwt') {
729
+ if (typeof as.jwks_uri !== 'string') {
730
+ throw new TypeError('"issuer.jwks_uri" must be a string');
731
+ }
732
+ const { claims } = await validateJwt(await preserveBodyStream(response).text(), checkSigningAlgorithm.bind(undefined, client.userinfo_signed_response_alg, as.userinfo_signing_alg_values_supported), getPublicSigKeyFromIssuerJwksUri.bind(undefined, as, options))
733
+ .then(validateOptionalAudience.bind(undefined, client.client_id))
734
+ .then(validateOptionalIssuer.bind(undefined, as.issuer));
735
+ json = claims;
736
+ }
737
+ else {
738
+ if (client.userinfo_signed_response_alg) {
739
+ throw new OPE('JWT UserInfo Response expected');
740
+ }
741
+ try {
742
+ json = await preserveBodyStream(response).json();
743
+ }
744
+ catch {
745
+ throw new OPE('failed to parse "response" body as JSON');
746
+ }
747
+ }
748
+ if (!isJsonObject(json)) {
749
+ throw new OPE('"response" body must be a top level object');
750
+ }
751
+ if (!validateString(json.sub)) {
752
+ throw new OPE('"response" body "sub" property must be a non-empty string');
753
+ }
754
+ switch (expectedSubject) {
755
+ case skipSubjectCheck:
756
+ break;
757
+ default:
758
+ if (!validateString(expectedSubject)) {
759
+ throw new OPE('"expectedSubject" must be a non-empty string');
760
+ }
761
+ if (json.sub !== expectedSubject) {
762
+ throw new OPE('unexpected "response" body "sub" value');
763
+ }
764
+ }
765
+ return json;
766
+ }
767
+ function padded(buf, length) {
768
+ const out = new Uint8Array(length);
769
+ out.set(buf);
770
+ return out;
771
+ }
772
+ function timingSafeEqual(a, b) {
773
+ const len = Math.max(a.byteLength, b.byteLength);
774
+ a = padded(a, len);
775
+ b = padded(b, len);
776
+ let out = 0;
777
+ let i = -1;
778
+ while (++i < len) {
779
+ out |= a[i] ^ b[i];
780
+ }
781
+ return out === 0;
782
+ }
783
+ async function idTokenHash(alg, data) {
784
+ let algorithm;
785
+ switch (alg) {
786
+ case 'RS256':
787
+ case 'PS256':
788
+ case 'ES256':
789
+ algorithm = { name: 'SHA-256' };
790
+ break;
791
+ case 'EdDSA':
792
+ algorithm = { name: 'SHA-512' };
793
+ break;
794
+ default:
795
+ throw new UnsupportedOperationError();
796
+ }
797
+ const digest = await crypto.subtle.digest(algorithm, buf(data));
798
+ return b64u(digest.slice(0, digest.byteLength / 2));
799
+ }
800
+ async function idTokenHashMatches(alg, data, actual) {
801
+ const expected = await idTokenHash(alg, data);
802
+ return timingSafeEqual(buf(actual), buf(expected));
803
+ }
804
+ async function authenticatedRequest(as, client, method, url, body, headers, options) {
805
+ await clientAuthentication(as, client, body, headers, options?.clientPrivateKey);
806
+ return fetch(url.href, {
807
+ body,
808
+ headers,
809
+ method,
810
+ redirect: 'manual',
811
+ signal: options?.signal ? signal(options.signal) : null,
812
+ }).then(processDpopNonce);
813
+ }
814
+ async function tokenEndpointRequest(as, client, grantType, parameters, options) {
815
+ if (typeof as.token_endpoint !== 'string') {
816
+ throw new TypeError('"issuer.token_endpoint" must be a string');
817
+ }
818
+ const url = new URL(as.token_endpoint);
819
+ parameters.set('grant_type', grantType);
820
+ const headers = prepareHeaders(options?.headers);
821
+ headers.set('accept', 'application/json');
822
+ if (options?.DPoP !== undefined) {
823
+ await dpopProofJwt(headers, options.DPoP, url, 'POST');
824
+ }
825
+ return authenticatedRequest(as, client, 'POST', url, parameters, headers, options);
826
+ }
827
+ export async function refreshTokenGrantRequest(as, client, refreshToken, options) {
828
+ assertIssuer(as);
829
+ assertClient(client);
830
+ if (!validateString(refreshToken)) {
831
+ throw new TypeError('"refreshToken" must be a non-empty string');
832
+ }
833
+ const parameters = new URLSearchParams(options?.additionalParameters);
834
+ parameters.set('refresh_token', refreshToken);
835
+ return tokenEndpointRequest(as, client, 'refresh_token', parameters, options);
836
+ }
837
+ const idTokenClaims = new WeakMap();
838
+ export function getValidatedIdTokenClaims(ref) {
839
+ if (!idTokenClaims.has(ref)) {
840
+ throw new TypeError('"ref" was already garbage collected or did not resolve from the proper sources');
841
+ }
842
+ return idTokenClaims.get(ref);
843
+ }
844
+ async function processGenericAccessTokenResponse(as, client, response, options, ignoreIdToken = false, ignoreRefreshToken = false) {
845
+ assertIssuer(as);
846
+ assertClient(client);
847
+ if (!(response instanceof Response)) {
848
+ throw new TypeError('"response" must be an instance of Response');
849
+ }
850
+ if (response.status !== 200) {
851
+ let err;
852
+ if ((err = await handleOAuthBodyError(response))) {
853
+ return err;
854
+ }
855
+ throw new OPE('"response" is not a conform Token Endpoint response');
856
+ }
857
+ let json;
858
+ try {
859
+ json = await preserveBodyStream(response).json();
860
+ }
861
+ catch {
862
+ throw new OPE('failed to parse "response" body as JSON');
863
+ }
864
+ if (!isJsonObject(json)) {
865
+ throw new OPE('"response" body must be a top level object');
866
+ }
867
+ if (!validateString(json.access_token)) {
868
+ throw new OPE('"response" body "access_token" property must be a non-empty string');
869
+ }
870
+ if (!validateString(json.token_type)) {
871
+ throw new OPE('"response" body "token_type" property must be a non-empty string');
872
+ }
873
+ json.token_type = json.token_type.toLowerCase();
874
+ if (json.token_type !== 'dpop' && json.token_type !== 'bearer') {
875
+ throw new UnsupportedOperationError('unsupported `token_type` value');
876
+ }
877
+ if (json.expires_in !== undefined &&
878
+ (typeof json.expires_in !== 'number' || json.expires_in <= 0)) {
879
+ throw new OPE('"response" body "expires_in" property must be a positive number');
880
+ }
881
+ if (!ignoreRefreshToken &&
882
+ json.refresh_token !== undefined &&
883
+ !validateString(json.refresh_token)) {
884
+ throw new OPE('"response" body "refresh_token" property must be a non-empty string');
885
+ }
886
+ if (json.scope !== undefined && typeof json.scope !== 'string') {
887
+ throw new OPE('"response" body "scope" property must be a string');
888
+ }
889
+ if (!ignoreIdToken) {
890
+ if (json.id_token !== undefined && !validateString(json.id_token)) {
891
+ throw new OPE('"response" body "id_token" property must be a non-empty string');
892
+ }
893
+ if (json.id_token) {
894
+ if (typeof as.jwks_uri !== 'string') {
895
+ throw new TypeError('"issuer.jwks_uri" must be a string');
896
+ }
897
+ const { header, claims } = await validateJwt(json.id_token, checkSigningAlgorithm.bind(undefined, client.id_token_signed_response_alg, as.id_token_signing_alg_values_supported), getPublicSigKeyFromIssuerJwksUri.bind(undefined, as, options))
898
+ .then(validatePresence.bind(undefined, ['aud', 'exp', 'iat', 'iss', 'sub']))
899
+ .then(validateIssuer.bind(undefined, as.issuer))
900
+ .then(validateAudience.bind(undefined, client.client_id));
901
+ if (Array.isArray(claims.aud) && claims.aud.length !== 1 && claims.azp !== client.client_id) {
902
+ throw new OPE('unexpected ID Token "azp" (authorized party) claim value');
903
+ }
904
+ if (client.require_auth_time && typeof claims.auth_time !== 'number') {
905
+ throw new OPE('unexpected ID Token "auth_time" (authentication time) claim value');
906
+ }
907
+ if (claims.at_hash !== undefined) {
908
+ if (typeof claims.at_hash !== 'string' ||
909
+ !(await idTokenHashMatches(header.alg, json.access_token, claims.at_hash))) {
910
+ throw new OPE('unexpected ID Token "at_hash" (access token hash) claim value');
911
+ }
912
+ }
913
+ idTokenClaims.set(json, claims);
914
+ }
915
+ }
916
+ return json;
917
+ }
918
+ export async function processRefreshTokenResponse(as, client, response, options) {
919
+ return processGenericAccessTokenResponse(as, client, response, options);
920
+ }
921
+ function validateOptionalAudience(expected, result) {
922
+ if (result.claims.aud !== undefined) {
923
+ return validateAudience(expected, result);
924
+ }
925
+ return result;
926
+ }
927
+ function validateAudience(expected, result) {
928
+ if (Array.isArray(result.claims.aud)) {
929
+ if (!result.claims.aud.includes(expected)) {
930
+ throw new OPE('unexpected JWT "aud" (audience) claim value');
931
+ }
932
+ }
933
+ else if (result.claims.aud !== expected) {
934
+ throw new OPE('unexpected JWT "aud" (audience) claim value');
935
+ }
936
+ return result;
937
+ }
938
+ function validateOptionalIssuer(expected, result) {
939
+ if (result.claims.iss !== undefined) {
940
+ return validateIssuer(expected, result);
941
+ }
942
+ return result;
943
+ }
944
+ function validateIssuer(expected, result) {
945
+ if (result.claims.iss !== expected) {
946
+ throw new OPE('unexpected JWT "iss" (issuer) claim value');
947
+ }
948
+ return result;
949
+ }
950
+ export async function authorizationCodeGrantRequest(as, client, callbackParameters, redirectUri, codeVerifier, options) {
951
+ assertIssuer(as);
952
+ assertClient(client);
953
+ if (!(callbackParameters instanceof CallbackParameters)) {
954
+ throw new TypeError('"callbackParameters" must be an instance of CallbackParameters obtained from "validateAuthResponse()", or "validateJwtAuthResponse()');
955
+ }
956
+ if (!validateString(redirectUri)) {
957
+ throw new TypeError('"redirectUri" must be a non-empty string');
958
+ }
959
+ if (!validateString(codeVerifier)) {
960
+ throw new TypeError('"codeVerifier" must be a non-empty string');
961
+ }
962
+ const code = getURLSearchParameter(callbackParameters, 'code');
963
+ if (!code) {
964
+ throw new OPE('no authorization code in "callbackParameters"');
965
+ }
966
+ const parameters = new URLSearchParams(options?.additionalParameters);
967
+ parameters.set('redirect_uri', redirectUri);
968
+ parameters.set('code_verifier', codeVerifier);
969
+ parameters.set('code', code);
970
+ return tokenEndpointRequest(as, client, 'authorization_code', parameters, options);
971
+ }
972
+ const claimNames = {
973
+ aud: 'audience',
974
+ exp: 'expiration time',
975
+ iat: 'issued at',
976
+ iss: 'issuer',
977
+ sub: 'subject',
978
+ };
979
+ function validatePresence(required, result) {
980
+ for (const claim of required) {
981
+ if (result.claims[claim] === undefined) {
982
+ throw new OPE(`JWT "${claim}" (${claimNames[claim]}) claim missing`);
983
+ }
984
+ }
985
+ return result;
986
+ }
987
+ export const expectNoNonce = Symbol();
988
+ export const skipAuthTimeCheck = Symbol();
989
+ export async function processAuthorizationCodeOpenIDResponse(as, client, response, expectedNonce, maxAge, options) {
990
+ const result = await processGenericAccessTokenResponse(as, client, response, options);
991
+ if (isOAuth2Error(result)) {
992
+ return result;
993
+ }
994
+ if (!validateString(result.id_token)) {
995
+ throw new OPE('"response" body "id_token" property must be a non-empty string');
996
+ }
997
+ maxAge ?? (maxAge = client.default_max_age ?? skipAuthTimeCheck);
998
+ const claims = getValidatedIdTokenClaims(result);
999
+ if ((client.require_auth_time || maxAge !== skipAuthTimeCheck) &&
1000
+ claims.auth_time === undefined) {
1001
+ throw new OPE('ID Token "auth_time" (authentication time) claim missing');
1002
+ }
1003
+ if (maxAge !== skipAuthTimeCheck) {
1004
+ if (typeof maxAge !== 'number' || maxAge < 0) {
1005
+ throw new TypeError('"options.max_age" must be a non-negative number');
1006
+ }
1007
+ const now = epochTime();
1008
+ const tolerance = 30;
1009
+ if (claims.auth_time + maxAge < now - tolerance) {
1010
+ throw new OPE('too much time has elapsed since the last End-User authentication');
1011
+ }
1012
+ }
1013
+ switch (expectedNonce) {
1014
+ case undefined:
1015
+ case expectNoNonce:
1016
+ if (claims.nonce !== undefined) {
1017
+ throw new OPE('unexpected ID Token "nonce" claim value');
1018
+ }
1019
+ break;
1020
+ default:
1021
+ if (!validateString(expectedNonce)) {
1022
+ throw new TypeError('"expectedNonce" must be a non-empty string');
1023
+ }
1024
+ if (claims.nonce === undefined) {
1025
+ throw new OPE('ID Token "nonce" claim missing');
1026
+ }
1027
+ if (claims.nonce !== expectedNonce) {
1028
+ throw new OPE('unexpected ID Token "nonce" claim value');
1029
+ }
1030
+ }
1031
+ return result;
1032
+ }
1033
+ export async function processAuthorizationCodeOAuth2Response(as, client, response) {
1034
+ const result = await processGenericAccessTokenResponse(as, client, response, undefined, true);
1035
+ if (isOAuth2Error(result)) {
1036
+ return result;
1037
+ }
1038
+ if (result.id_token !== undefined) {
1039
+ if (typeof result.id_token === 'string' && result.id_token.length) {
1040
+ throw new OPE('Unexpected ID Token returned, use processAuthorizationCodeOpenIDResponse() for OpenID Connect callback processing');
1041
+ }
1042
+ delete result.id_token;
1043
+ }
1044
+ return result;
1045
+ }
1046
+ function checkJwtType(expected, result) {
1047
+ if (typeof result.header.typ !== 'string' || normalizeTyp(result.header.typ) !== expected) {
1048
+ throw new OPE('unexpected JWT "typ" header parameter value');
1049
+ }
1050
+ return result;
1051
+ }
1052
+ export async function clientCredentialsGrantRequest(as, client, parameters, options) {
1053
+ assertIssuer(as);
1054
+ assertClient(client);
1055
+ return tokenEndpointRequest(as, client, 'client_credentials', new URLSearchParams(parameters), options);
1056
+ }
1057
+ export async function processClientCredentialsResponse(as, client, response) {
1058
+ const result = await processGenericAccessTokenResponse(as, client, response, undefined, true, true);
1059
+ if (isOAuth2Error(result)) {
1060
+ return result;
1061
+ }
1062
+ return result;
1063
+ }
1064
+ export async function revocationRequest(as, client, token, options) {
1065
+ assertIssuer(as);
1066
+ assertClient(client);
1067
+ if (!validateString(token)) {
1068
+ throw new TypeError('"token" must be a non-empty string');
1069
+ }
1070
+ if (typeof as.revocation_endpoint !== 'string') {
1071
+ throw new TypeError('"issuer.revocation_endpoint" must be a string');
1072
+ }
1073
+ const url = new URL(as.revocation_endpoint);
1074
+ const body = new URLSearchParams(options?.additionalParameters);
1075
+ body.set('token', token);
1076
+ const headers = prepareHeaders(options?.headers);
1077
+ headers.delete('accept');
1078
+ return authenticatedRequest(as, client, 'POST', url, body, headers, options);
1079
+ }
1080
+ export async function processRevocationResponse(response) {
1081
+ if (!(response instanceof Response)) {
1082
+ throw new TypeError('"response" must be an instance of Response');
1083
+ }
1084
+ if (response.status !== 200) {
1085
+ let err;
1086
+ if ((err = await handleOAuthBodyError(response))) {
1087
+ return err;
1088
+ }
1089
+ throw new OPE('"response" is not a conform Revocation Endpoint response');
1090
+ }
1091
+ return undefined;
1092
+ }
1093
+ function assertReadableResponse(response) {
1094
+ if (response.bodyUsed) {
1095
+ throw new TypeError('"response" body has been used already');
1096
+ }
1097
+ }
1098
+ export async function introspectionRequest(as, client, token, options) {
1099
+ assertIssuer(as);
1100
+ assertClient(client);
1101
+ if (!validateString(token)) {
1102
+ throw new TypeError('"token" must be a non-empty string');
1103
+ }
1104
+ if (typeof as.introspection_endpoint !== 'string') {
1105
+ throw new TypeError('"issuer.introspection_endpoint" must be a string');
1106
+ }
1107
+ const url = new URL(as.introspection_endpoint);
1108
+ const body = new URLSearchParams(options?.additionalParameters);
1109
+ body.set('token', token);
1110
+ const headers = prepareHeaders(options?.headers);
1111
+ if (options?.requestJwtResponse ?? client.introspection_signed_response_alg) {
1112
+ headers.set('accept', 'application/token-introspection+jwt');
1113
+ }
1114
+ else {
1115
+ headers.set('accept', 'application/json');
1116
+ }
1117
+ return authenticatedRequest(as, client, 'POST', url, body, headers, options);
1118
+ }
1119
+ export async function processIntrospectionResponse(as, client, response, options) {
1120
+ assertIssuer(as);
1121
+ assertClient(client);
1122
+ if (!(response instanceof Response)) {
1123
+ throw new TypeError('"response" must be an instance of Response');
1124
+ }
1125
+ if (response.status !== 200) {
1126
+ let err;
1127
+ if ((err = await handleOAuthBodyError(response))) {
1128
+ return err;
1129
+ }
1130
+ throw new OPE('"response" is not a conform Introspection Endpoint response');
1131
+ }
1132
+ let json;
1133
+ if (getContentType(response) === 'application/token-introspection+jwt') {
1134
+ if (typeof as.jwks_uri !== 'string') {
1135
+ throw new TypeError('"issuer.jwks_uri" must be a string');
1136
+ }
1137
+ const { claims } = await validateJwt(await preserveBodyStream(response).text(), checkSigningAlgorithm.bind(undefined, client.introspection_signed_response_alg, as.introspection_signing_alg_values_supported), getPublicSigKeyFromIssuerJwksUri.bind(undefined, as, options))
1138
+ .then(checkJwtType.bind(undefined, 'token-introspection+jwt'))
1139
+ .then(validatePresence.bind(undefined, ['aud', 'iat', 'iss']))
1140
+ .then(validateIssuer.bind(undefined, as.issuer))
1141
+ .then(validateAudience.bind(undefined, client.client_id));
1142
+ json = claims.token_introspection;
1143
+ if (!isJsonObject(json)) {
1144
+ throw new OPE('JWT "token_introspection" claim must be a JSON object');
1145
+ }
1146
+ }
1147
+ else {
1148
+ try {
1149
+ json = await preserveBodyStream(response).json();
1150
+ }
1151
+ catch {
1152
+ throw new OPE('failed to parse "response" body as JSON');
1153
+ }
1154
+ if (!isJsonObject(json)) {
1155
+ throw new OPE('"response" body must be a top level object');
1156
+ }
1157
+ }
1158
+ if (typeof json.active !== 'boolean') {
1159
+ throw new OPE('"response" body "active" property must be a boolean');
1160
+ }
1161
+ return json;
1162
+ }
1163
+ export async function jwksRequest(as, options) {
1164
+ assertIssuer(as);
1165
+ if (typeof as.jwks_uri !== 'string') {
1166
+ throw new TypeError('"issuer.jwks_uri" must be a string');
1167
+ }
1168
+ const url = new URL(as.jwks_uri);
1169
+ const headers = prepareHeaders(options?.headers);
1170
+ headers.set('accept', 'application/json');
1171
+ headers.append('accept', 'application/jwk-set+json');
1172
+ return fetch(url.href, {
1173
+ headers,
1174
+ method: 'GET',
1175
+ redirect: 'manual',
1176
+ signal: options?.signal ? signal(options.signal) : null,
1177
+ }).then(processDpopNonce);
1178
+ }
1179
+ export async function processJwksResponse(response) {
1180
+ if (!(response instanceof Response)) {
1181
+ throw new TypeError('"response" must be an instance of Response');
1182
+ }
1183
+ if (response.status !== 200) {
1184
+ throw new OPE('"response" is not a conform JSON Web Key Set response');
1185
+ }
1186
+ let json;
1187
+ try {
1188
+ json = await preserveBodyStream(response).json();
1189
+ }
1190
+ catch {
1191
+ throw new OPE('failed to parse "response" body as JSON');
1192
+ }
1193
+ if (!isJsonObject(json)) {
1194
+ throw new OPE('"response" body must be a top level object');
1195
+ }
1196
+ if (!Array.isArray(json.keys)) {
1197
+ throw new OPE('"response" body "keys" property must be an array');
1198
+ }
1199
+ if (!Array.prototype.every.call(json.keys, isJsonObject)) {
1200
+ throw new OPE('"response" body "keys" property members must be JWK formatted objects');
1201
+ }
1202
+ return json;
1203
+ }
1204
+ async function handleOAuthBodyError(response) {
1205
+ if (response.status > 399 && response.status < 500) {
1206
+ try {
1207
+ const json = await preserveBodyStream(response).json();
1208
+ if (isJsonObject(json) && typeof json.error === 'string' && json.error.length) {
1209
+ if (json.error_description !== undefined && typeof json.error_description !== 'string') {
1210
+ delete json.error_description;
1211
+ }
1212
+ if (json.error_uri !== undefined && typeof json.error_uri !== 'string') {
1213
+ delete json.error_uri;
1214
+ }
1215
+ if (json.algs !== undefined && typeof json.algs !== 'string') {
1216
+ delete json.algs;
1217
+ }
1218
+ if (json.scope !== undefined && typeof json.scope !== 'string') {
1219
+ delete json.scope;
1220
+ }
1221
+ return json;
1222
+ }
1223
+ }
1224
+ catch { }
1225
+ }
1226
+ return undefined;
1227
+ }
1228
+ function checkSupportedJwsAlg(alg) {
1229
+ if (!SUPPORTED_JWS_ALGS.includes(alg)) {
1230
+ throw new UnsupportedOperationError('unsupported JWS "alg" identifier');
1231
+ }
1232
+ return alg;
1233
+ }
1234
+ function checkRsaKeyAlgorithm(algorithm) {
1235
+ if (typeof algorithm.modulusLength !== 'number' || algorithm.modulusLength < 2048) {
1236
+ throw new OPE(`${algorithm.name} modulusLength must be at least 2048 bits`);
1237
+ }
1238
+ }
1239
+ function subtleAlgorithm(key) {
1240
+ switch (key.algorithm.name) {
1241
+ case 'ECDSA':
1242
+ return { name: key.algorithm.name, hash: { name: 'SHA-256' } };
1243
+ case 'RSA-PSS':
1244
+ checkRsaKeyAlgorithm(key.algorithm);
1245
+ return {
1246
+ name: key.algorithm.name,
1247
+ saltLength: 256 >> 3,
1248
+ };
1249
+ case 'RSASSA-PKCS1-v1_5':
1250
+ checkRsaKeyAlgorithm(key.algorithm);
1251
+ return { name: key.algorithm.name };
1252
+ case 'Ed25519':
1253
+ return { name: key.algorithm.name };
1254
+ }
1255
+ throw new UnsupportedOperationError();
1256
+ }
1257
+ async function validateJwt(jws, checkAlg, getKey) {
1258
+ const { 0: protectedHeader, 1: payload, 2: signature, length } = jws.split('.');
1259
+ if (length === 5) {
1260
+ throw new UnsupportedOperationError('JWE structure JWTs are not supported');
1261
+ }
1262
+ if (length !== 3) {
1263
+ throw new OPE('Invalid JWT');
1264
+ }
1265
+ let header;
1266
+ try {
1267
+ header = JSON.parse(buf(b64u(protectedHeader)));
1268
+ }
1269
+ catch {
1270
+ throw new OPE('failed to parse JWT Header body as base64url encoded JSON');
1271
+ }
1272
+ if (!isJsonObject(header)) {
1273
+ throw new OPE('JWT Header must be a top level object');
1274
+ }
1275
+ checkAlg(header);
1276
+ if (header.crit !== undefined) {
1277
+ throw new OPE('unexpected JWT "crit" header parameter');
1278
+ }
1279
+ const key = await getKey(header);
1280
+ const input = `${protectedHeader}.${payload}`;
1281
+ const verified = await crypto.subtle.verify(subtleAlgorithm(key), key, b64u(signature), buf(input));
1282
+ if (!verified) {
1283
+ throw new OPE('JWT signature verification failed');
1284
+ }
1285
+ let claims;
1286
+ try {
1287
+ claims = JSON.parse(buf(b64u(payload)));
1288
+ }
1289
+ catch {
1290
+ throw new OPE('failed to parse JWT Payload body as base64url encoded JSON');
1291
+ }
1292
+ if (!isJsonObject(claims)) {
1293
+ throw new OPE('JWT Payload must be a top level object');
1294
+ }
1295
+ const now = epochTime();
1296
+ const tolerance = 30;
1297
+ if (claims.exp !== undefined) {
1298
+ if (typeof claims.exp !== 'number') {
1299
+ throw new OPE('unexpected JWT "exp" (expiration time) claim type');
1300
+ }
1301
+ if (claims.exp <= now - tolerance) {
1302
+ throw new OPE('unexpected JWT "exp" (expiration time) claim value, timestamp is <= now()');
1303
+ }
1304
+ }
1305
+ if (claims.iat !== undefined) {
1306
+ if (typeof claims.iat !== 'number') {
1307
+ throw new OPE('unexpected JWT "iat" (issued at) claim type');
1308
+ }
1309
+ }
1310
+ if (claims.iss !== undefined) {
1311
+ if (typeof claims.iss !== 'string') {
1312
+ throw new OPE('unexpected JWT "iss" (issuer) claim type');
1313
+ }
1314
+ }
1315
+ if (claims.nbf !== undefined) {
1316
+ if (typeof claims.nbf !== 'number') {
1317
+ throw new OPE('unexpected JWT "nbf" (not before) claim type');
1318
+ }
1319
+ if (claims.nbf > now + tolerance) {
1320
+ throw new OPE('unexpected JWT "nbf" (not before) claim value, timestamp is > now()');
1321
+ }
1322
+ }
1323
+ if (claims.aud !== undefined) {
1324
+ if (typeof claims.aud !== 'string' && !Array.isArray(claims.aud)) {
1325
+ throw new OPE('unexpected JWT "aud" (audience) claim type');
1326
+ }
1327
+ }
1328
+ return { header, claims };
1329
+ }
1330
+ export async function validateJwtAuthResponse(as, client, parameters, expectedState, options) {
1331
+ assertIssuer(as);
1332
+ assertClient(client);
1333
+ if (parameters instanceof URL) {
1334
+ parameters = parameters.searchParams;
1335
+ }
1336
+ if (!(parameters instanceof URLSearchParams)) {
1337
+ throw new TypeError('"parameters" must be an instance of URLSearchParams, or URL');
1338
+ }
1339
+ const response = getURLSearchParameter(parameters, 'response');
1340
+ if (!response) {
1341
+ throw new OPE('"parameters" does not contain a JARM response');
1342
+ }
1343
+ if (typeof as.jwks_uri !== 'string') {
1344
+ throw new TypeError('"issuer.jwks_uri" must be a string');
1345
+ }
1346
+ const { claims } = await validateJwt(response, checkSigningAlgorithm.bind(undefined, client.authorization_signed_response_alg, as.authorization_signing_alg_values_supported), getPublicSigKeyFromIssuerJwksUri.bind(undefined, as, options))
1347
+ .then(validatePresence.bind(undefined, ['aud', 'exp', 'iss']))
1348
+ .then(validateIssuer.bind(undefined, as.issuer))
1349
+ .then(validateAudience.bind(undefined, client.client_id));
1350
+ const result = new URLSearchParams();
1351
+ for (const [key, value] of Object.entries(claims)) {
1352
+ if (typeof value === 'string' && key !== 'aud') {
1353
+ result.set(key, value);
1354
+ }
1355
+ }
1356
+ return validateAuthResponse(as, client, result, expectedState);
1357
+ }
1358
+ function checkSigningAlgorithm(client, issuer, header) {
1359
+ if (client !== undefined) {
1360
+ if (header.alg !== client) {
1361
+ throw new OPE('unexpected JWT "alg" header parameter');
1362
+ }
1363
+ return;
1364
+ }
1365
+ if (Array.isArray(issuer)) {
1366
+ if (!issuer.includes(header.alg)) {
1367
+ throw new OPE('unexpected JWT "alg" header parameter');
1368
+ }
1369
+ return;
1370
+ }
1371
+ if (header.alg !== 'RS256') {
1372
+ throw new OPE('unexpected JWT "alg" header parameter');
1373
+ }
1374
+ }
1375
+ function getURLSearchParameter(parameters, name) {
1376
+ const { 0: value, length } = parameters.getAll(name);
1377
+ if (length > 1) {
1378
+ throw new OPE(`"${name}" parameter must be provided only once`);
1379
+ }
1380
+ return value;
1381
+ }
1382
+ export const skipStateCheck = Symbol();
1383
+ export const expectNoState = Symbol();
1384
+ class CallbackParameters extends URLSearchParams {
1385
+ }
1386
+ export function validateAuthResponse(as, client, parameters, expectedState) {
1387
+ assertIssuer(as);
1388
+ assertClient(client);
1389
+ if (parameters instanceof URL) {
1390
+ parameters = parameters.searchParams;
1391
+ }
1392
+ if (!(parameters instanceof URLSearchParams)) {
1393
+ throw new TypeError('"parameters" must be an instance of URLSearchParams, or URL');
1394
+ }
1395
+ if (getURLSearchParameter(parameters, 'response')) {
1396
+ throw new OPE('"parameters" contains a JARM response, use validateJwtAuthResponse() instead of validateAuthResponse()');
1397
+ }
1398
+ const iss = getURLSearchParameter(parameters, 'iss');
1399
+ const state = getURLSearchParameter(parameters, 'state');
1400
+ if (!iss && as.authorization_response_iss_parameter_supported) {
1401
+ throw new OPE('response parameter "iss" (issuer) missing');
1402
+ }
1403
+ if (iss && iss !== as.issuer) {
1404
+ throw new OPE('unexpected "iss" (issuer) response parameter value');
1405
+ }
1406
+ switch (expectedState) {
1407
+ case undefined:
1408
+ case expectNoState:
1409
+ if (state !== undefined) {
1410
+ throw new OPE('unexpected "state" response parameter encountered');
1411
+ }
1412
+ break;
1413
+ case skipStateCheck:
1414
+ break;
1415
+ default:
1416
+ if (!validateString(expectedState)) {
1417
+ throw new OPE('"expectedState" must be a non-empty string');
1418
+ }
1419
+ if (state === undefined) {
1420
+ throw new OPE('response parameter "state" missing');
1421
+ }
1422
+ if (state !== expectedState) {
1423
+ throw new OPE('unexpected "state" response parameter value');
1424
+ }
1425
+ }
1426
+ const error = getURLSearchParameter(parameters, 'error');
1427
+ if (error) {
1428
+ return {
1429
+ error,
1430
+ error_description: getURLSearchParameter(parameters, 'error_description'),
1431
+ error_uri: getURLSearchParameter(parameters, 'error_uri'),
1432
+ };
1433
+ }
1434
+ const id_token = getURLSearchParameter(parameters, 'id_token');
1435
+ const token = getURLSearchParameter(parameters, 'token');
1436
+ if (id_token !== undefined || token !== undefined) {
1437
+ throw new UnsupportedOperationError('implicit and hybrid flows are not supported');
1438
+ }
1439
+ return new CallbackParameters(parameters);
1440
+ }
1441
+ async function importJwk(jwk) {
1442
+ const { alg, ext, key_ops, use, ...key } = jwk;
1443
+ let algorithm;
1444
+ switch (alg) {
1445
+ case 'PS256':
1446
+ algorithm = { name: 'RSA-PSS', hash: { name: 'SHA-256' } };
1447
+ break;
1448
+ case 'RS256':
1449
+ algorithm = { name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-256' } };
1450
+ break;
1451
+ case 'ES256':
1452
+ algorithm = { name: 'ECDSA', namedCurve: 'P-256' };
1453
+ break;
1454
+ case 'EdDSA':
1455
+ algorithm = { name: 'Ed25519' };
1456
+ break;
1457
+ default:
1458
+ throw new UnsupportedOperationError();
1459
+ }
1460
+ return crypto.subtle.importKey('jwk', key, algorithm, true, ['verify']);
1461
+ }
1462
+ export async function deviceAuthorizationRequest(as, client, parameters, options) {
1463
+ assertIssuer(as);
1464
+ assertClient(client);
1465
+ if (!(parameters instanceof URLSearchParams)) {
1466
+ throw new TypeError('"parameters" must be an instance of URLSearchParams');
1467
+ }
1468
+ if (typeof as.device_authorization_endpoint !== 'string') {
1469
+ throw new TypeError('"issuer.device_authorization_endpoint" must be a string');
1470
+ }
1471
+ const url = new URL(as.device_authorization_endpoint);
1472
+ const body = new URLSearchParams(parameters);
1473
+ body.set('client_id', client.client_id);
1474
+ const headers = prepareHeaders(options?.headers);
1475
+ headers.set('accept', 'application/json');
1476
+ return authenticatedRequest(as, client, 'POST', url, body, headers, options);
1477
+ }
1478
+ export async function processDeviceAuthorizationResponse(as, client, response) {
1479
+ assertIssuer(as);
1480
+ assertClient(client);
1481
+ if (!(response instanceof Response)) {
1482
+ throw new TypeError('"response" must be an instance of Response');
1483
+ }
1484
+ if (response.status !== 200) {
1485
+ let err;
1486
+ if ((err = await handleOAuthBodyError(response))) {
1487
+ return err;
1488
+ }
1489
+ throw new OPE('"response" is not a conform Device Authorization Endpoint response');
1490
+ }
1491
+ let json;
1492
+ try {
1493
+ json = await preserveBodyStream(response).json();
1494
+ }
1495
+ catch {
1496
+ throw new OPE('failed to parse "response" body as JSON');
1497
+ }
1498
+ if (!isJsonObject(json)) {
1499
+ throw new OPE('"response" body must be a top level object');
1500
+ }
1501
+ if (!validateString(json.device_code)) {
1502
+ throw new OPE('"response" body "device_code" property must be a non-empty string');
1503
+ }
1504
+ if (!validateString(json.user_code)) {
1505
+ throw new OPE('"response" body "user_code" property must be a non-empty string');
1506
+ }
1507
+ if (!validateString(json.verification_uri)) {
1508
+ throw new OPE('"response" body "verification_uri" property must be a non-empty string');
1509
+ }
1510
+ if (typeof json.expires_in !== 'number' || json.expires_in <= 0) {
1511
+ throw new OPE('"response" body "expires_in" property must be a positive number');
1512
+ }
1513
+ if (json.verification_uri_complete !== undefined &&
1514
+ !validateString(json.verification_uri_complete)) {
1515
+ throw new OPE('"response" body "verification_uri_complete" property must be a non-empty string');
1516
+ }
1517
+ if (json.interval !== undefined && (typeof json.interval !== 'number' || json.interval <= 0)) {
1518
+ throw new OPE('"response" body "interval" property must be a positive number');
1519
+ }
1520
+ return json;
1521
+ }
1522
+ export async function deviceCodeGrantRequest(as, client, deviceCode, options) {
1523
+ assertIssuer(as);
1524
+ assertClient(client);
1525
+ if (!validateString(deviceCode)) {
1526
+ throw new TypeError('"deviceCode" must be a non-empty string');
1527
+ }
1528
+ const parameters = new URLSearchParams(options?.additionalParameters);
1529
+ parameters.set('device_code', deviceCode);
1530
+ return tokenEndpointRequest(as, client, 'urn:ietf:params:oauth:grant-type:device_code', parameters, options);
1531
+ }
1532
+ export async function processDeviceCodeResponse(as, client, response, options) {
1533
+ return processGenericAccessTokenResponse(as, client, response, options);
1534
+ }
1535
+ export async function generateKeyPair(alg, options) {
1536
+ let algorithm;
1537
+ if (!validateString(alg)) {
1538
+ throw new TypeError('"alg" must be a non-empty string');
1539
+ }
1540
+ switch (alg) {
1541
+ case 'PS256':
1542
+ algorithm = {
1543
+ name: 'RSA-PSS',
1544
+ hash: { name: 'SHA-256' },
1545
+ modulusLength: options?.modulusLength ?? 2048,
1546
+ publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
1547
+ };
1548
+ break;
1549
+ case 'RS256':
1550
+ algorithm = {
1551
+ name: 'RSASSA-PKCS1-v1_5',
1552
+ hash: { name: 'SHA-256' },
1553
+ modulusLength: options?.modulusLength ?? 2048,
1554
+ publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
1555
+ };
1556
+ break;
1557
+ case 'ES256':
1558
+ algorithm = { name: 'ECDSA', namedCurve: 'P-256' };
1559
+ break;
1560
+ case 'EdDSA':
1561
+ algorithm = { name: 'Ed25519' };
1562
+ break;
1563
+ default:
1564
+ throw new UnsupportedOperationError();
1565
+ }
1566
+ return (crypto.subtle.generateKey(algorithm, options?.extractable ?? false, ['sign', 'verify']));
1567
+ }
1568
+ export async function calculateJwkThumbprint(key) {
1569
+ if (!isPublicKey(key) || !key.extractable) {
1570
+ throw new TypeError('"key" must be an extractable public CryptoKey');
1571
+ }
1572
+ determineJWSAlgorithm(key);
1573
+ const jwk = await crypto.subtle.exportKey('jwk', key);
1574
+ let components;
1575
+ switch (jwk.kty) {
1576
+ case 'EC':
1577
+ components = { crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y };
1578
+ break;
1579
+ case 'OKP':
1580
+ components = { crv: jwk.crv, kty: jwk.kty, x: jwk.x };
1581
+ break;
1582
+ case 'RSA':
1583
+ components = { e: jwk.e, kty: jwk.kty, n: jwk.n };
1584
+ break;
1585
+ default:
1586
+ throw new UnsupportedOperationError();
1587
+ }
1588
+ return b64u(await crypto.subtle.digest({ name: 'SHA-256' }, buf(JSON.stringify(components))));
1589
+ }