oauth4webapi 2.4.4 → 2.5.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 CHANGED
@@ -43,7 +43,7 @@ import * as oauth2 from 'oauth4webapi'
43
43
  **`example`** Deno import
44
44
 
45
45
  ```js
46
- import * as oauth2 from 'https://deno.land/x/oauth4webapi@v2.4.4/mod.ts'
46
+ import * as oauth2 from 'https://deno.land/x/oauth4webapi@v2.5.0/mod.ts'
47
47
  ```
48
48
 
49
49
  - Authorization Code Flow - OpenID Connect [source](examples/code.ts), or plain OAuth 2 [source](examples/oauth.ts)
@@ -74,6 +74,5 @@ The following features are currently out of scope:
74
74
 
75
75
  - CommonJS
76
76
  - Implicit, Hybrid, and Resource Owner Password Credentials Flows
77
- - Mutual-TLS Client Authentication and Certificate-Bound Access Tokens
78
77
  - JSON Web Encryption (JWE)
79
78
  - Automatic polyfills of any kind
package/build/index.d.ts CHANGED
@@ -127,10 +127,160 @@ export type ClientAuthenticationMethod = 'client_secret_basic' | 'client_secret_
127
127
  * ```
128
128
  */
129
129
  export type JWSAlgorithm = 'PS256' | 'ES256' | 'RS256' | 'EdDSA' | 'ES384' | 'PS384' | 'RS384' | 'ES512' | 'PS512' | 'RS512';
130
- /** @ignore during Documentation generation but part of the public API */
131
130
  export declare const clockSkew: unique symbol;
132
- /** @ignore during Documentation generation but part of the public API */
133
131
  export declare const clockTolerance: unique symbol;
132
+ /**
133
+ * This is an experimental feature, it is not subject to semantic versioning rules. Non-backward
134
+ * compatible changes or removal may occur in any future release.
135
+ *
136
+ * When configured on an interface that extends {@link HttpRequestOptions}, that's every `options`
137
+ * parameter for functions that trigger HTTP Requests, this replaces the use of global fetch. As a
138
+ * fetch replacement the arguments and expected return are the same as fetch.
139
+ *
140
+ * In theory any module that claims to be compatible with the Fetch API can be used but your mileage
141
+ * may vary. No workarounds to allow use of non-conform {@link Response}s will be considered.
142
+ *
143
+ * If you only need to update the {@link Request} properties you do not need to use a Fetch API
144
+ * module, just change what you need and pass it to globalThis.fetch just like this module would
145
+ * normally do.
146
+ *
147
+ * Its intended use cases are:
148
+ *
149
+ * - {@link Request}/{@link Response} tracing and logging
150
+ * - Custom caching strategies for responses of Authorization Server Metadata and JSON Web Key Set
151
+ * (JWKS) endpoints
152
+ * - Changing the {@link Request} properties like headers, body, credentials, mode before it is passed
153
+ * to fetch
154
+ *
155
+ * Known caveats:
156
+ *
157
+ * - Expect Type-related issues when passing the inputs through to fetch-like modules, they hardly
158
+ * ever get their typings inline with actual fetch, you should `@ts-expect-error` them.
159
+ * - Returning self-constructed {@link Response} instances prohibits AS-signalled DPoP Nonce caching.
160
+ *
161
+ * @example
162
+ *
163
+ * Using [sindresorhus/ky](https://github.com/sindresorhus/ky) hooks feature for logging outgoing
164
+ * requests and their responses.
165
+ *
166
+ * ```js
167
+ * import ky from 'ky'
168
+ * import * as oauth from 'oauth4webapi'
169
+ *
170
+ * // example use
171
+ * await oauth.discoveryRequest(new URL('https://as.example.com'), {
172
+ * [oauth.experimentalCustomFetch]: (...args) =>
173
+ * ky(args[0], {
174
+ * ...args[1],
175
+ * hooks: {
176
+ * beforeRequest: [
177
+ * (request) => {
178
+ * logRequest(request)
179
+ * },
180
+ * ],
181
+ * beforeRetry: [
182
+ * ({ request, error, retryCount }) => {
183
+ * logRetry(request, error, retryCount)
184
+ * },
185
+ * ],
186
+ * afterResponse: [
187
+ * (request, _, response) => {
188
+ * logResponse(request, response)
189
+ * },
190
+ * ],
191
+ * },
192
+ * }),
193
+ * })
194
+ * ```
195
+ *
196
+ * @example
197
+ *
198
+ * Using [nodejs/undici](https://github.com/nodejs/undici) for mocking.
199
+ *
200
+ * ```js
201
+ * import * as undici from 'undici'
202
+ * import * as oauth from 'oauth4webapi'
203
+ *
204
+ * const mockAgent = new undici.MockAgent()
205
+ * mockAgent.disableNetConnect()
206
+ * undici.setGlobalDispatcher(mockAgent)
207
+ *
208
+ * // continue as per undici documentation
209
+ * // https://github.com/nodejs/undici/blob/v6.2.1/docs/api/MockAgent.md#example---basic-mocked-request
210
+ *
211
+ * // example use
212
+ * await oauth.discoveryRequest(new URL('https://as.example.com'), {
213
+ * [oauth.experimentalCustomFetch]: undici.fetch,
214
+ * })
215
+ * ```
216
+ */
217
+ export declare const experimentalCustomFetch: unique symbol;
218
+ /**
219
+ * This is an experimental feature, it is not subject to semantic versioning rules. Non-backward
220
+ * compatible changes or removal may occur in any future release.
221
+ *
222
+ * When combined with {@link experimentalCustomFetch} (to use a Fetch API implementation that
223
+ * supports client certificates) this can be used to target FAPI 2.0 profiles that utilize
224
+ * Mutual-TLS for either client authentication or sender constraining. FAPI 1.0 Advanced profiles
225
+ * that use PAR and JARM can also be targetted.
226
+ *
227
+ * When configured on an interface that extends {@link ExperimentalUseMTLSAliasOptions} this makes
228
+ * the client prioritize an endpoint URL present in
229
+ * {@link AuthorizationServer.mtls_endpoint_aliases `as.mtls_endpoint_aliases`}.
230
+ *
231
+ * @example
232
+ *
233
+ * (Node.js) Using [nodejs/undici](https://github.com/nodejs/undici) for Mutual-TLS Client
234
+ * Authentication and Certificate-Bound Access Tokens support.
235
+ *
236
+ * ```js
237
+ * import * as undici from 'undici'
238
+ * import * as oauth from 'oauth4webapi'
239
+ *
240
+ * const response = await oauth.pushedAuthorizationRequest(as, client, params, {
241
+ * [oauth.experimentalUseMtlsAlias]: true,
242
+ * [oauth.experimentalCustomFetch]: (...args) => {
243
+ * return undici.fetch(args[0], {
244
+ * ...args[1],
245
+ * dispatcher: new undici.Agent({
246
+ * connect: {
247
+ * key: clientKey,
248
+ * cert: clientCertificate,
249
+ * },
250
+ * }),
251
+ * })
252
+ * },
253
+ * })
254
+ * ```
255
+ *
256
+ * @example
257
+ *
258
+ * (Deno) Using Deno.createHttpClient API for Mutual-TLS Client Authentication and Certificate-Bound
259
+ * Access Tokens support. This is currently (Jan 2023) locked behind the --unstable command line
260
+ * flag.
261
+ *
262
+ * ```js
263
+ * import * as oauth from 'oauth4webapi'
264
+ *
265
+ * const agent = Deno.createHttpClient({
266
+ * certChain: clientCertificate,
267
+ * privateKey: clientKey,
268
+ * })
269
+ *
270
+ * const response = await oauth.pushedAuthorizationRequest(as, client, params, {
271
+ * [oauth.experimentalUseMtlsAlias]: true,
272
+ * [oauth.experimentalCustomFetch]: (...args) => {
273
+ * return fetch(args[0], {
274
+ * ...args[1],
275
+ * client: agent,
276
+ * })
277
+ * },
278
+ * })
279
+ * ```
280
+ *
281
+ * @see [RFC 8705 - OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens](https://www.rfc-editor.org/rfc/rfc8705.html)
282
+ */
283
+ export declare const experimentalUseMtlsAlias: unique symbol;
134
284
  /**
135
285
  * Authorization Server Metadata
136
286
  *
@@ -456,9 +606,9 @@ export interface Client {
456
606
  * Use to adjust the client's assumed current time. Positive and negative finite values
457
607
  * representing seconds are allowed. Default is `0` (Date.now() + 0 seconds is used).
458
608
  *
459
- * @ignore during Documentation generation but part of the public API
609
+ * @example
460
610
  *
461
- * @example Client's local clock is mistakenly 1 hour in the past
611
+ * When the client's local clock is mistakenly 1 hour in the past
462
612
  *
463
613
  * ```ts
464
614
  * const client: oauth.Client = {
@@ -468,7 +618,9 @@ export interface Client {
468
618
  * }
469
619
  * ```
470
620
  *
471
- * @example Client's local clock is mistakenly 1 hour in the future
621
+ * @example
622
+ *
623
+ * When the client's local clock is mistakenly 1 hour in the future
472
624
  *
473
625
  * ```ts
474
626
  * const client: oauth.Client = {
@@ -483,9 +635,9 @@ export interface Client {
483
635
  * Use to set allowed client's clock tolerance when checking DateTime JWT Claims. Only positive
484
636
  * finite values representing seconds are allowed. Default is `30` (30 seconds).
485
637
  *
486
- * @ignore during Documentation generation but part of the public API
638
+ * @example
487
639
  *
488
- * @example Tolerate 30 seconds clock skew when validating JWT claims like exp or nbf.
640
+ * Tolerate 30 seconds clock skew when validating JWT claims like exp or nbf.
489
641
  *
490
642
  * ```ts
491
643
  * const client: oauth.Client = {
@@ -522,11 +674,15 @@ export interface HttpRequestOptions {
522
674
  * ```
523
675
  */
524
676
  signal?: (() => AbortSignal) | AbortSignal;
677
+ /** Headers to additionally send with the HTTP Request(s) triggered by this function's invocation. */
678
+ headers?: [string, string][] | Record<string, string> | Headers;
525
679
  /**
526
- * A Headers instance to additionally send with the HTTP Request(s) triggered by this function's
527
- * invocation.
680
+ * This is an experimental feature, it is not subject to semantic versioning rules. Non-backward
681
+ * compatible changes or removal may occur in any future release.
682
+ *
683
+ * See {@link experimentalCustomFetch} for its documentation.
528
684
  */
529
- headers?: Headers;
685
+ [experimentalCustomFetch]?: typeof fetch;
530
686
  }
531
687
  export interface DiscoveryRequestOptions extends HttpRequestOptions {
532
688
  /** The issuer transformation algorithm to use. */
@@ -625,7 +781,16 @@ export interface DPoPRequestOptions {
625
781
  /** DPoP-related options. */
626
782
  DPoP?: DPoPOptions;
627
783
  }
628
- export interface AuthenticatedRequestOptions {
784
+ export interface ExperimentalUseMTLSAliasOptions {
785
+ /**
786
+ * This is an experimental feature, it is not subject to semantic versioning rules. Non-backward
787
+ * compatible changes or removal may occur in any future release.
788
+ *
789
+ * See {@link experimentalUseMtlsAlias} for its documentation.
790
+ */
791
+ [experimentalUseMtlsAlias]?: boolean;
792
+ }
793
+ export interface AuthenticatedRequestOptions extends ExperimentalUseMTLSAliasOptions {
629
794
  /**
630
795
  * Private key to use for `private_key_jwt`
631
796
  * {@link ClientAuthenticationMethod client authentication}. Its algorithm must be compatible with
@@ -747,10 +912,8 @@ export interface ProtectedResourceRequestOptions extends Omit<HttpRequestOptions
747
912
  *
748
913
  * This option only affects the request if the {@link ProtectedResourceRequestOptions.DPoP DPoP}
749
914
  * option is also used.
750
- *
751
- * @ignore during Documentation generation but part of the public API
752
915
  */
753
- clockSkew?: number;
916
+ [clockSkew]?: number;
754
917
  }
755
918
  /**
756
919
  * Performs a protected resource request at an arbitrary URL.
@@ -769,7 +932,7 @@ export interface ProtectedResourceRequestOptions extends Omit<HttpRequestOptions
769
932
  * @see [RFC 9449 - OAuth 2.0 Demonstrating Proof-of-Possession at the Application Layer (DPoP)](https://www.rfc-editor.org/rfc/rfc9449.html#name-protected-resource-access)
770
933
  */
771
934
  export declare function protectedResourceRequest(accessToken: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | string, url: URL, headers: Headers, body?: ReadableStream | Blob | ArrayBufferView | ArrayBuffer | FormData | URLSearchParams | string | null, options?: ProtectedResourceRequestOptions): Promise<Response>;
772
- export interface UserInfoRequestOptions extends HttpRequestOptions, DPoPRequestOptions {
935
+ export interface UserInfoRequestOptions extends HttpRequestOptions, DPoPRequestOptions, ExperimentalUseMTLSAliasOptions {
773
936
  }
774
937
  /**
775
938
  * Performs a UserInfo Request at the
package/build/index.js CHANGED
@@ -1,11 +1,25 @@
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 = 'v2.4.4';
4
+ const VERSION = 'v2.5.0';
5
5
  USER_AGENT = `${NAME}/${VERSION}`;
6
6
  }
7
+ function looseInstanceOf(input, expected) {
8
+ if (input == null) {
9
+ return false;
10
+ }
11
+ try {
12
+ return (input instanceof expected ||
13
+ Object.getPrototypeOf(input)[Symbol.toStringTag] === expected.prototype[Symbol.toStringTag]);
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }
7
19
  export const clockSkew = Symbol();
8
20
  export const clockTolerance = Symbol();
21
+ export const experimentalCustomFetch = Symbol();
22
+ export const experimentalUseMtlsAlias = Symbol();
9
23
  const encoder = new TextEncoder();
10
24
  const decoder = new TextDecoder();
11
25
  function buf(input) {
@@ -148,8 +162,8 @@ function isJsonObject(input) {
148
162
  return true;
149
163
  }
150
164
  function prepareHeaders(input) {
151
- if (input !== undefined && !(input instanceof Headers)) {
152
- throw new TypeError('"options.headers" must be an instance of Headers');
165
+ if (looseInstanceOf(input, Headers)) {
166
+ input = Object.fromEntries(input.entries());
153
167
  }
154
168
  const headers = new Headers(input);
155
169
  if (USER_AGENT && !headers.has('user-agent')) {
@@ -198,13 +212,12 @@ export async function discoveryRequest(issuerIdentifier, options) {
198
212
  }
199
213
  const headers = prepareHeaders(options?.headers);
200
214
  headers.set('accept', 'application/json');
201
- const request = new Request(url.href, {
202
- headers,
215
+ return (options?.[experimentalCustomFetch] || fetch)(url.href, {
216
+ headers: Object.fromEntries(headers.entries()),
203
217
  method: 'GET',
204
218
  redirect: 'manual',
205
219
  signal: options?.signal ? signal(options.signal) : null,
206
- });
207
- return fetch(request).then(processDpopNonce);
220
+ }).then(processDpopNonce);
208
221
  }
209
222
  function validateString(input) {
210
223
  return typeof input === 'string' && input.length !== 0;
@@ -213,7 +226,7 @@ export async function processDiscoveryResponse(expectedIssuerIdentifier, respons
213
226
  if (!(expectedIssuerIdentifier instanceof URL)) {
214
227
  throw new TypeError('"expectedIssuer" must be an instance of URL');
215
228
  }
216
- if (!(response instanceof Response)) {
229
+ if (!looseInstanceOf(response, Response)) {
217
230
  throw new TypeError('"response" must be an instance of Response');
218
231
  }
219
232
  if (response.status !== 200) {
@@ -427,9 +440,11 @@ async function clientAuthentication(as, client, body, headers, clientPrivateKey)
427
440
  body.set('client_assertion', await privateKeyJwt(as, client, key, kid));
428
441
  break;
429
442
  }
443
+ case 'tls_client_auth':
444
+ case 'self_signed_tls_client_auth':
430
445
  case 'none': {
431
- assertNoClientSecret('none', client.client_secret);
432
- assertNoClientPrivateKey('none', clientPrivateKey);
446
+ assertNoClientSecret(client.token_endpoint_auth_method, client.client_secret);
447
+ assertNoClientPrivateKey(client.token_endpoint_auth_method, clientPrivateKey);
433
448
  body.set('client_id', client.client_id);
434
449
  break;
435
450
  }
@@ -531,13 +546,29 @@ async function publicJwk(key) {
531
546
  jwkCache.set(key, jwk);
532
547
  return jwk;
533
548
  }
549
+ function validateEndpoint(value, endpoint, options) {
550
+ if (typeof value !== 'string') {
551
+ if (options?.[experimentalUseMtlsAlias]) {
552
+ throw new TypeError(`"as.mtls_endpoint_aliases.${endpoint}" must be a string`);
553
+ }
554
+ else {
555
+ throw new TypeError(`"as.${endpoint}" must be a string`);
556
+ }
557
+ }
558
+ return new URL(value);
559
+ }
560
+ function resolveEndpoint(as, endpoint, options) {
561
+ if (options?.[experimentalUseMtlsAlias] &&
562
+ as.mtls_endpoint_aliases &&
563
+ endpoint in as.mtls_endpoint_aliases) {
564
+ return validateEndpoint(as.mtls_endpoint_aliases[endpoint], endpoint, options);
565
+ }
566
+ return validateEndpoint(as[endpoint], endpoint);
567
+ }
534
568
  export async function pushedAuthorizationRequest(as, client, parameters, options) {
535
569
  assertAs(as);
536
570
  assertClient(client);
537
- if (typeof as.pushed_authorization_request_endpoint !== 'string') {
538
- throw new TypeError('"as.pushed_authorization_request_endpoint" must be a string');
539
- }
540
- const url = new URL(as.pushed_authorization_request_endpoint);
571
+ const url = resolveEndpoint(as, 'pushed_authorization_request_endpoint', options);
541
572
  const body = new URLSearchParams(parameters);
542
573
  body.set('client_id', client.client_id);
543
574
  const headers = prepareHeaders(options?.headers);
@@ -585,7 +616,7 @@ function wwwAuth(scheme, params) {
585
616
  };
586
617
  }
587
618
  export function parseWwwAuthenticateChallenges(response) {
588
- if (!(response instanceof Response)) {
619
+ if (!looseInstanceOf(response, Response)) {
589
620
  throw new TypeError('"response" must be an instance of Response');
590
621
  }
591
622
  if (!response.headers.has('www-authenticate')) {
@@ -615,7 +646,7 @@ export function parseWwwAuthenticateChallenges(response) {
615
646
  export async function processPushedAuthorizationResponse(as, client, response) {
616
647
  assertAs(as);
617
648
  assertClient(client);
618
- if (!(response instanceof Response)) {
649
+ if (!looseInstanceOf(response, Response)) {
619
650
  throw new TypeError('"response" must be an instance of Response');
620
651
  }
621
652
  if (response.status !== 201) {
@@ -656,25 +687,21 @@ export async function protectedResourceRequest(accessToken, method, url, headers
656
687
  headers.set('authorization', `Bearer ${accessToken}`);
657
688
  }
658
689
  else {
659
- await dpopProofJwt(headers, options.DPoP, url, 'GET', getClockSkew({ [clockSkew]: options?.clockSkew }), accessToken);
690
+ await dpopProofJwt(headers, options.DPoP, url, 'GET', getClockSkew({ [clockSkew]: options?.[clockSkew] }), accessToken);
660
691
  headers.set('authorization', `DPoP ${accessToken}`);
661
692
  }
662
- const request = new Request(url.href, {
693
+ return (options?.[experimentalCustomFetch] || fetch)(url.href, {
663
694
  body,
664
- headers,
695
+ headers: Object.fromEntries(headers.entries()),
665
696
  method,
666
697
  redirect: 'manual',
667
698
  signal: options?.signal ? signal(options.signal) : null,
668
- });
669
- return fetch(request).then(processDpopNonce);
699
+ }).then(processDpopNonce);
670
700
  }
671
701
  export async function userInfoRequest(as, client, accessToken, options) {
672
702
  assertAs(as);
673
703
  assertClient(client);
674
- if (typeof as.userinfo_endpoint !== 'string') {
675
- throw new TypeError('"as.userinfo_endpoint" must be a string');
676
- }
677
- const url = new URL(as.userinfo_endpoint);
704
+ const url = resolveEndpoint(as, 'userinfo_endpoint', options);
678
705
  const headers = prepareHeaders(options?.headers);
679
706
  if (client.userinfo_signed_response_alg) {
680
707
  headers.set('accept', 'application/jwt');
@@ -685,7 +712,7 @@ export async function userInfoRequest(as, client, accessToken, options) {
685
712
  }
686
713
  return protectedResourceRequest(accessToken, 'GET', url, headers, null, {
687
714
  ...options,
688
- clockSkew: getClockSkew(client),
715
+ [clockSkew]: getClockSkew(client),
689
716
  });
690
717
  }
691
718
  let jwksCache;
@@ -778,7 +805,7 @@ function getContentType(response) {
778
805
  export async function processUserInfoResponse(as, client, expectedSubject, response) {
779
806
  assertAs(as);
780
807
  assertClient(client);
781
- if (!(response instanceof Response)) {
808
+ if (!looseInstanceOf(response, Response)) {
782
809
  throw new TypeError('"response" must be an instance of Response');
783
810
  }
784
811
  if (response.status !== 200) {
@@ -826,20 +853,16 @@ export async function processUserInfoResponse(as, client, expectedSubject, respo
826
853
  async function authenticatedRequest(as, client, method, url, body, headers, options) {
827
854
  await clientAuthentication(as, client, body, headers, options?.clientPrivateKey);
828
855
  headers.set('content-type', 'application/x-www-form-urlencoded;charset=UTF-8');
829
- const request = new Request(url.href, {
856
+ return (options?.[experimentalCustomFetch] || fetch)(url.href, {
830
857
  body,
831
- headers,
858
+ headers: Object.fromEntries(headers.entries()),
832
859
  method,
833
860
  redirect: 'manual',
834
861
  signal: options?.signal ? signal(options.signal) : null,
835
- });
836
- return fetch(request).then(processDpopNonce);
862
+ }).then(processDpopNonce);
837
863
  }
838
864
  async function tokenEndpointRequest(as, client, grantType, parameters, options) {
839
- if (typeof as.token_endpoint !== 'string') {
840
- throw new TypeError('"as.token_endpoint" must be a string');
841
- }
842
- const url = new URL(as.token_endpoint);
865
+ const url = resolveEndpoint(as, 'token_endpoint', options);
843
866
  parameters.set('grant_type', grantType);
844
867
  const headers = prepareHeaders(options?.headers);
845
868
  headers.set('accept', 'application/json');
@@ -872,7 +895,7 @@ export function getValidatedIdTokenClaims(ref) {
872
895
  async function processGenericAccessTokenResponse(as, client, response, ignoreIdToken = false, ignoreRefreshToken = false) {
873
896
  assertAs(as);
874
897
  assertClient(client);
875
- if (!(response instanceof Response)) {
898
+ if (!looseInstanceOf(response, Response)) {
876
899
  throw new TypeError('"response" must be an instance of Response');
877
900
  }
878
901
  if (response.status !== 200) {
@@ -1092,10 +1115,7 @@ export async function revocationRequest(as, client, token, options) {
1092
1115
  if (!validateString(token)) {
1093
1116
  throw new TypeError('"token" must be a non-empty string');
1094
1117
  }
1095
- if (typeof as.revocation_endpoint !== 'string') {
1096
- throw new TypeError('"as.revocation_endpoint" must be a string');
1097
- }
1098
- const url = new URL(as.revocation_endpoint);
1118
+ const url = resolveEndpoint(as, 'revocation_endpoint', options);
1099
1119
  const body = new URLSearchParams(options?.additionalParameters);
1100
1120
  body.set('token', token);
1101
1121
  const headers = prepareHeaders(options?.headers);
@@ -1103,7 +1123,7 @@ export async function revocationRequest(as, client, token, options) {
1103
1123
  return authenticatedRequest(as, client, 'POST', url, body, headers, options);
1104
1124
  }
1105
1125
  export async function processRevocationResponse(response) {
1106
- if (!(response instanceof Response)) {
1126
+ if (!looseInstanceOf(response, Response)) {
1107
1127
  throw new TypeError('"response" must be an instance of Response');
1108
1128
  }
1109
1129
  if (response.status !== 200) {
@@ -1126,10 +1146,7 @@ export async function introspectionRequest(as, client, token, options) {
1126
1146
  if (!validateString(token)) {
1127
1147
  throw new TypeError('"token" must be a non-empty string');
1128
1148
  }
1129
- if (typeof as.introspection_endpoint !== 'string') {
1130
- throw new TypeError('"as.introspection_endpoint" must be a string');
1131
- }
1132
- const url = new URL(as.introspection_endpoint);
1149
+ const url = resolveEndpoint(as, 'introspection_endpoint', options);
1133
1150
  const body = new URLSearchParams(options?.additionalParameters);
1134
1151
  body.set('token', token);
1135
1152
  const headers = prepareHeaders(options?.headers);
@@ -1144,7 +1161,7 @@ export async function introspectionRequest(as, client, token, options) {
1144
1161
  export async function processIntrospectionResponse(as, client, response) {
1145
1162
  assertAs(as);
1146
1163
  assertClient(client);
1147
- if (!(response instanceof Response)) {
1164
+ if (!looseInstanceOf(response, Response)) {
1148
1165
  throw new TypeError('"response" must be an instance of Response');
1149
1166
  }
1150
1167
  if (response.status !== 200) {
@@ -1186,23 +1203,19 @@ export async function processIntrospectionResponse(as, client, response) {
1186
1203
  }
1187
1204
  async function jwksRequest(as, options) {
1188
1205
  assertAs(as);
1189
- if (typeof as.jwks_uri !== 'string') {
1190
- throw new TypeError('"as.jwks_uri" must be a string');
1191
- }
1192
- const url = new URL(as.jwks_uri);
1206
+ const url = resolveEndpoint(as, 'jwks_uri');
1193
1207
  const headers = prepareHeaders(options?.headers);
1194
1208
  headers.set('accept', 'application/json');
1195
1209
  headers.append('accept', 'application/jwk-set+json');
1196
- const request = new Request(url.href, {
1197
- headers,
1210
+ return (options?.[experimentalCustomFetch] || fetch)(url.href, {
1211
+ headers: Object.fromEntries(headers.entries()),
1198
1212
  method: 'GET',
1199
1213
  redirect: 'manual',
1200
1214
  signal: options?.signal ? signal(options.signal) : null,
1201
- });
1202
- return fetch(request).then(processDpopNonce);
1215
+ }).then(processDpopNonce);
1203
1216
  }
1204
1217
  async function processJwksResponse(response) {
1205
- if (!(response instanceof Response)) {
1218
+ if (!looseInstanceOf(response, Response)) {
1206
1219
  throw new TypeError('"response" must be an instance of Response');
1207
1220
  }
1208
1221
  if (response.status !== 200) {
@@ -1525,10 +1538,7 @@ async function importJwk(alg, jwk) {
1525
1538
  export async function deviceAuthorizationRequest(as, client, parameters, options) {
1526
1539
  assertAs(as);
1527
1540
  assertClient(client);
1528
- if (typeof as.device_authorization_endpoint !== 'string') {
1529
- throw new TypeError('"as.device_authorization_endpoint" must be a string');
1530
- }
1531
- const url = new URL(as.device_authorization_endpoint);
1541
+ const url = resolveEndpoint(as, 'device_authorization_endpoint', options);
1532
1542
  const body = new URLSearchParams(parameters);
1533
1543
  body.set('client_id', client.client_id);
1534
1544
  const headers = prepareHeaders(options?.headers);
@@ -1538,7 +1548,7 @@ export async function deviceAuthorizationRequest(as, client, parameters, options
1538
1548
  export async function processDeviceAuthorizationResponse(as, client, response) {
1539
1549
  assertAs(as);
1540
1550
  assertClient(client);
1541
- if (!(response instanceof Response)) {
1551
+ if (!looseInstanceOf(response, Response)) {
1542
1552
  throw new TypeError('"response" must be an instance of Response');
1543
1553
  }
1544
1554
  if (response.status !== 200) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oauth4webapi",
3
- "version": "2.4.4",
3
+ "version": "2.5.0",
4
4
  "description": "OAuth 2 / OpenID Connect for JavaScript Runtimes",
5
5
  "keywords": [
6
6
  "auth",
@@ -80,6 +80,7 @@
80
80
  "puppeteer-core": "^21.7.0",
81
81
  "qunit": "^2.20.0",
82
82
  "raw-body": "^2.5.2",
83
+ "selfsigned": "^2.4.1",
83
84
  "timekeeper": "^2.3.1",
84
85
  "tsx": "^4.7.0",
85
86
  "typedoc": "^0.25.7",