diff --git a/package-lock.json b/package-lock.json index e804ae73ab..49a5fcf16a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23016,7 +23016,6 @@ "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", - "dev": true, "license": "MIT", "engines": { "node": ">=20.18.1" @@ -23970,6 +23969,7 @@ "@inrupt/solid-client-authn-core": "^3.1.1", "jose": "^5.1.3", "openid-client": "^5.7.1", + "undici": "^7.0.0", "uuid": "^11.1.0" }, "devDependencies": { diff --git a/packages/core/src/Session.ts b/packages/core/src/Session.ts index 4758355789..9db317e382 100644 --- a/packages/core/src/Session.ts +++ b/packages/core/src/Session.ts @@ -20,4 +20,6 @@ export type SessionConfig = { keepAlive: boolean; + /** Custom fetch function used as the underlying HTTP transport (e.g., HTTP/2). */ + customFetch?: typeof fetch; }; diff --git a/packages/core/src/login/ILoginOptions.ts b/packages/core/src/login/ILoginOptions.ts index 8be9360706..a1969dd8ae 100644 --- a/packages/core/src/login/ILoginOptions.ts +++ b/packages/core/src/login/ILoginOptions.ts @@ -57,4 +57,9 @@ export default interface ILoginOptions extends ILoginInputOptions { * Whether the session is refreshed in the background or not. */ keepAlive?: boolean; + + /** + * Custom fetch function used as the underlying HTTP transport (e.g., HTTP/2). + */ + customFetch?: typeof fetch; } diff --git a/packages/core/src/login/oidc/IOidcOptions.ts b/packages/core/src/login/oidc/IOidcOptions.ts index 8dade63854..5e7a9e92a1 100644 --- a/packages/core/src/login/oidc/IOidcOptions.ts +++ b/packages/core/src/login/oidc/IOidcOptions.ts @@ -74,6 +74,11 @@ export interface IOidcOptions { * The Authorization Request OAuth scopes. */ scopes: string[]; + + /** + * Custom fetch function used as the underlying HTTP transport (e.g., HTTP/2). + */ + customFetch?: typeof fetch; } export function normalizeScopes(scopes: string[] | undefined): string[] { diff --git a/packages/node/README.md b/packages/node/README.md index 3fb1785667..8d0fecb2f9 100644 --- a/packages/node/README.md +++ b/packages/node/README.md @@ -3,6 +3,59 @@ `solid-client-authn-node` is a library designed to authenticate Node.js apps (both scripts and full-blown Web servers) with Solid identity servers. The main documentation is at the [root of the repository](https://github.com/inrupt/solid-client-authn-js). +## HTTP/2 Support + +Node.js `fetch` defaults to HTTP/1.1, which limits concurrent requests to ~6 TCP connections per origin. When interacting with Solid pods, applications frequently issue many requests in parallel (fetching resources, ACLs, containers, profiles, etc.). + +This library provides built-in HTTP/2 support that multiplexes all requests to the same origin over a single TCP connection, significantly reducing latency for concurrent workloads. + +> **Note:** Browsers already negotiate HTTP/2 transparently. This feature is Node.js-only. + +### Using HTTP/2 with Session + +Pass `http2: true` when creating a Session. All authenticated and unauthenticated requests made through `session.fetch` will use HTTP/2 multiplexing. Connections are automatically cleaned up on logout. + +```typescript +import { Session } from "@inrupt/solid-client-authn-node"; +import { getSolidDataset } from "@inrupt/solid-client"; + +const session = new Session({ http2: true }); +await session.login({ + clientId: "...", + clientSecret: "...", + oidcIssuer: "https://login.inrupt.com", +}); + +// These requests multiplex over a single HTTP/2 connection +const [dataset, profile, container] = await Promise.all([ + getSolidDataset(url1, { fetch: session.fetch }), + getSolidDataset(url2, { fetch: session.fetch }), + getSolidDataset(url3, { fetch: session.fetch }), +]); + +await session.logout(); // Also closes HTTP/2 connections +``` + +### Standalone HTTP/2 fetch + +For use cases outside of `Session` (e.g. unauthenticated requests or custom auth), you can use `createHttp2Fetch` directly: + +```typescript +import { createHttp2Fetch } from "@inrupt/solid-client-authn-node"; +import { getSolidDataset } from "@inrupt/solid-client"; + +const h2fetch = createHttp2Fetch(); + +const dataset = await getSolidDataset(url, { fetch: h2fetch }); + +// Clean up when done +h2fetch.close(); +``` + +### DPoP and Bearer compatibility + +HTTP/2 support works with both DPoP and Bearer token authentication. The `buildAuthenticatedFetch` layer generates per-request DPoP proofs as usual; the HTTP/2 transport is transparent to the auth layer. + ## Underlying libraries `solid-client-authn-node` is based on [`jose`](https://github.com/panva/jose). diff --git a/packages/node/package.json b/packages/node/package.json index 542da728b7..eb4ed907e2 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -31,6 +31,7 @@ "@inrupt/solid-client-authn-core": "^3.1.1", "jose": "^5.1.3", "openid-client": "^5.7.1", + "undici": "^7.0.0", "uuid": "^11.1.0" }, "publishConfig": { diff --git a/packages/node/src/ClientAuthentication.ts b/packages/node/src/ClientAuthentication.ts index 790f59b211..8e085e9143 100644 --- a/packages/node/src/ClientAuthentication.ts +++ b/packages/node/src/ClientAuthentication.ts @@ -72,6 +72,7 @@ export default class ClientAuthentication extends ClientAuthenticationBase { eventEmitter, keepAlive: config.keepAlive, customScopes: options.customScopes, + customFetch: config.customFetch, }); if (loginReturn !== undefined) { diff --git a/packages/node/src/Session.ts b/packages/node/src/Session.ts index 5e79472e7e..4d49f8fc0d 100644 --- a/packages/node/src/Session.ts +++ b/packages/node/src/Session.ts @@ -45,6 +45,8 @@ import type ClientAuthentication from "./ClientAuthentication"; import { getClientAuthenticationWithDependencies } from "./dependencies"; import IssuerConfigFetcher from "./login/oidc/IssuerConfigFetcher"; import StorageUtilityNode from "./storage/StorageUtility"; +import type { Http2Fetch } from "./http2Fetch"; +import { createHttp2Fetch } from "./http2Fetch"; export interface ISessionOptions { /** @@ -84,6 +86,40 @@ export interface ISessionOptions { * A boolean flag indicating whether a session should be constantly kept alive in the background. */ keepAlive: boolean; + /** + * When `true`, the session uses HTTP/2 multiplexing for all requests. + * + * All requests to the same origin share a single TCP connection, enabling + * true parallel request execution. This can significantly reduce latency + * when issuing many concurrent requests to a Solid pod (fetching resources, + * ACLs, containers, profiles, etc.). + * + * HTTP/2 connections are automatically cleaned up when {@link Session.logout} + * is called. Both Bearer and DPoP authentication work transparently over + * the HTTP/2 transport. + * + * Browsers already negotiate HTTP/2 transparently, so this option only + * affects the Node.js environment. + * + * @default false + * @since 2.6.0 + * + * @example + * ```typescript + * const session = new Session({ http2: true }); + * await session.login({ ... }); + * + * // Requests issued in parallel multiplex over a single connection + * const [a, b, c] = await Promise.all([ + * getSolidDataset(url1, { fetch: session.fetch }), + * getSolidDataset(url2, { fetch: session.fetch }), + * getSolidDataset(url3, { fetch: session.fetch }), + * ]); + * + * await session.logout(); // closes HTTP/2 connections + * ``` + */ + http2: boolean; } /** @@ -133,6 +169,8 @@ export class Session implements IHasSessionEventListener { private clientAuthentication: ClientAuthentication; + private http2Fetch: Http2Fetch | undefined; + private tokenRequestInProgress = false; private lastTimeoutHandle = 0; @@ -354,9 +392,14 @@ export class Session implements IHasSessionEventListener { isLoggedIn: false, }; } + if (sessionOptions.http2) { + this.http2Fetch = createHttp2Fetch(); + } + this.config = { // Default to true for backwards compatibility. keepAlive: sessionOptions.keepAlive ?? true, + customFetch: this.http2Fetch, }; // Keeps track of the latest timeout handle in order to clean up on logout // and not leave open timeouts. @@ -414,7 +457,7 @@ export class Session implements IHasSessionEventListener { */ fetch: typeof fetch = async (url, init) => { if (!this.info.isLoggedIn) { - return fetch(url, init); + return (this.http2Fetch ?? fetch)(url, init); } return this.clientAuthentication.fetch(url, init); }; @@ -466,6 +509,10 @@ export class Session implements IHasSessionEventListener { await this.clientAuthentication.logout(this.info.sessionId, options); // Clears the timeouts on logout so that Node does not hang. clearTimeout(this.lastTimeoutHandle); + // Close HTTP/2 connections if active. + if (this.http2Fetch) { + this.http2Fetch.close(); + } this.info.isLoggedIn = false; if (emitEvent) { (this.events as EventEmitter).emit(EVENTS.LOGOUT); diff --git a/packages/node/src/http2Fetch.spec.ts b/packages/node/src/http2Fetch.spec.ts new file mode 100644 index 0000000000..dd4ec74324 --- /dev/null +++ b/packages/node/src/http2Fetch.spec.ts @@ -0,0 +1,264 @@ +// Copyright Inrupt Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +// Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import { describe, it, expect, afterEach } from "@jest/globals"; +import http2 from "node:http2"; +import fs from "node:fs"; +import path from "node:path"; +import { execSync } from "node:child_process"; +import { createHttp2Fetch } from "./http2Fetch"; +import type { Http2Fetch } from "./http2Fetch"; + +// Generate self-signed certs for testing +function generateSelfSignedCert(): { key: Buffer; cert: Buffer } { + const tmpDir = fs.mkdtempSync("/tmp/h2test-"); + execSync( + `openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -keyout ${tmpDir}/key.pem -out ${tmpDir}/cert.pem -days 1 -nodes -subj "/CN=localhost" 2>/dev/null`, + ); + return { + key: fs.readFileSync(path.join(tmpDir, "key.pem")), + cert: fs.readFileSync(path.join(tmpDir, "cert.pem")), + }; +} + +let server: http2.Http2SecureServer; +let serverPort: number; +let h2fetch: Http2Fetch; + +const certs = generateSelfSignedCert(); + +function createTestServer( + handler: ( + stream: http2.ServerHttp2Stream, + headers: http2.IncomingHttpHeaders, + ) => void, +): Promise { + return new Promise((resolve) => { + server = http2.createSecureServer({ + key: certs.key, + cert: certs.cert, + }); + server.on("stream", handler); + server.listen(0, () => { + const addr = server.address(); + if (addr && typeof addr === "object") { + resolve(addr.port); + } + }); + }); +} + +const testTlsOptions = { rejectUnauthorized: false }; + +afterEach(async () => { + if (h2fetch) { + h2fetch.close(); + } + if (server) { + await new Promise((resolve) => server.close(() => resolve())); + } +}); + +describe("createHttp2Fetch", () => { + it("returns a function with a close method", () => { + h2fetch = createHttp2Fetch({ tlsOptions: testTlsOptions }); + expect(typeof h2fetch).toBe("function"); + expect(typeof h2fetch.close).toBe("function"); + }); + + it("makes a successful GET request over HTTP/2", async () => { + serverPort = await createTestServer((stream, headers) => { + stream.respond({ + ":status": 200, + "content-type": "text/plain", + }); + stream.end("hello from h2"); + }); + + h2fetch = createHttp2Fetch({ tlsOptions: testTlsOptions }); + const response = await h2fetch(`https://localhost:${serverPort}/test`); + + expect(response.status).toBe(200); + expect(response.headers.get("content-type")).toBe("text/plain"); + const body = await response.text(); + expect(body).toBe("hello from h2"); + }); + + it("sends correct method and path", async () => { + let receivedHeaders: http2.IncomingHttpHeaders = {}; + + serverPort = await createTestServer((stream, headers) => { + receivedHeaders = headers; + stream.respond({ ":status": 204 }); + stream.end(); + }); + + h2fetch = createHttp2Fetch({ tlsOptions: testTlsOptions }); + await h2fetch(`https://localhost:${serverPort}/resource?q=test`, { + method: "POST", + }); + + expect(receivedHeaders[":method"]).toBe("POST"); + expect(receivedHeaders[":path"]).toBe("/resource?q=test"); + }); + + it("sends custom headers", async () => { + let receivedHeaders: http2.IncomingHttpHeaders = {}; + + serverPort = await createTestServer((stream, headers) => { + receivedHeaders = headers; + stream.respond({ ":status": 200 }); + stream.end(); + }); + + h2fetch = createHttp2Fetch({ tlsOptions: testTlsOptions }); + await h2fetch(`https://localhost:${serverPort}/`, { + headers: { + Authorization: "Bearer token123", + "X-Custom": "value", + }, + }); + + expect(receivedHeaders.authorization).toBe("Bearer token123"); + expect(receivedHeaders["x-custom"]).toBe("value"); + }); + + it("sends request body", async () => { + let receivedBody = ""; + + serverPort = await createTestServer((stream) => { + const chunks: Buffer[] = []; + stream.on("data", (chunk: Buffer) => chunks.push(chunk)); + stream.on("end", () => { + receivedBody = Buffer.concat(chunks).toString(); + stream.respond({ ":status": 200 }); + stream.end(); + }); + }); + + h2fetch = createHttp2Fetch({ tlsOptions: testTlsOptions }); + await h2fetch(`https://localhost:${serverPort}/`, { + method: "PUT", + body: '{"key": "value"}', + }); + + expect(receivedBody).toBe('{"key": "value"}'); + }); + + it("reuses connections for same origin (multiplexing)", async () => { + let requestCount = 0; + + serverPort = await createTestServer((stream) => { + requestCount++; + stream.respond({ ":status": 200 }); + stream.end(`response ${requestCount}`); + }); + + h2fetch = createHttp2Fetch({ tlsOptions: testTlsOptions }); + + // Issue multiple requests in parallel to the same origin + const responses = await Promise.all([ + h2fetch(`https://localhost:${serverPort}/a`), + h2fetch(`https://localhost:${serverPort}/b`), + h2fetch(`https://localhost:${serverPort}/c`), + ]); + + expect(responses).toHaveLength(3); + for (const resp of responses) { + expect(resp.status).toBe(200); + } + expect(requestCount).toBe(3); + }); + + it("handles server errors", async () => { + serverPort = await createTestServer((stream) => { + stream.respond({ ":status": 500 }); + stream.end("Internal Server Error"); + }); + + h2fetch = createHttp2Fetch({ tlsOptions: testTlsOptions }); + const response = await h2fetch(`https://localhost:${serverPort}/fail`); + + expect(response.status).toBe(500); + expect(response.ok).toBe(false); + }); + + it("close() shuts down all sessions", async () => { + serverPort = await createTestServer((stream) => { + stream.respond({ ":status": 200 }); + stream.end("ok"); + }); + + h2fetch = createHttp2Fetch({ tlsOptions: testTlsOptions }); + await h2fetch(`https://localhost:${serverPort}/test`); + + // Should not throw + h2fetch.close(); + + // After close, new requests should create new connections + const response = await h2fetch(`https://localhost:${serverPort}/test2`); + expect(response.status).toBe(200); + }); + + it("sets the response url property", async () => { + serverPort = await createTestServer((stream) => { + stream.respond({ ":status": 200 }); + stream.end(); + }); + + h2fetch = createHttp2Fetch({ tlsOptions: testTlsOptions }); + const response = await h2fetch( + `https://localhost:${serverPort}/my/resource`, + ); + + expect(response.url).toBe( + `https://localhost:${serverPort}/my/resource`, + ); + }); + + it("accepts URL objects as input", async () => { + serverPort = await createTestServer((stream) => { + stream.respond({ ":status": 200 }); + stream.end("ok"); + }); + + h2fetch = createHttp2Fetch({ tlsOptions: testTlsOptions }); + const url = new URL(`https://localhost:${serverPort}/test`); + const response = await h2fetch(url); + expect(response.status).toBe(200); + }); + + it("accepts Headers object in init", async () => { + let receivedHeaders: http2.IncomingHttpHeaders = {}; + + serverPort = await createTestServer((stream, headers) => { + receivedHeaders = headers; + stream.respond({ ":status": 200 }); + stream.end(); + }); + + h2fetch = createHttp2Fetch({ tlsOptions: testTlsOptions }); + const headers = new Headers(); + headers.set("Accept", "application/json"); + await h2fetch(`https://localhost:${serverPort}/`, { headers }); + + expect(receivedHeaders.accept).toBe("application/json"); + }); +}); diff --git a/packages/node/src/http2Fetch.ts b/packages/node/src/http2Fetch.ts new file mode 100644 index 0000000000..025149da77 --- /dev/null +++ b/packages/node/src/http2Fetch.ts @@ -0,0 +1,107 @@ +// Copyright Inrupt Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +// Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import { Agent, fetch as undiciFetch } from "undici"; +import type tls from "node:tls"; + +const DEFAULT_IDLE_TIMEOUT_MS = 30_000; + +/** + * A fetch-compatible function that uses HTTP/2 as the underlying transport. + * + * Callable with the same signature as the standard + * [fetch API](https://developer.mozilla.org/docs/Web/API/Fetch_API). + * Includes a {@link Http2Fetch.close | close()} method that shuts down all + * pooled HTTP/2 sessions. You should call `close()` when the fetch instance + * is no longer needed (e.g. on application shutdown) to release TCP connections + * and allow the Node.js process to exit cleanly. + * + * @since 2.6.0 + */ +export type Http2Fetch = typeof fetch & { + /** Closes all pooled HTTP/2 sessions and releases their TCP connections. */ + close(): void; +}; + +/** + * Creates an HTTP/2-aware fetch function compatible with the standard + * [fetch API](https://developer.mozilla.org/docs/Web/API/Fetch_API). + * + * All requests to the same origin share a single HTTP/2 connection, enabling + * multiplexing of concurrent requests. This provides significant performance + * improvements when making many requests to the same server, as is common + * when interacting with Solid pods. + * + * The returned function can be passed as the `fetch` option to any + * `@inrupt/solid-client` function, or used with `new Session({ http2: true })` + * for automatic integration. + * + * @param options Configuration for the HTTP/2 connection pool. + * @param options.idleTimeout Milliseconds of inactivity before an HTTP/2 + * session is closed. Defaults to 30 000 (30 seconds). + * @param options.tlsOptions TLS options forwarded to the underlying connection. + * Useful for custom CA certificates or disabling certificate verification + * in development/testing. + * @returns A fetch-compatible function with an additional `close()` method. + * + * @example + * ```typescript + * import { createHttp2Fetch } from "@inrupt/solid-client-authn-node"; + * import { getSolidDataset } from "@inrupt/solid-client"; + * + * const h2fetch = createHttp2Fetch(); + * const dataset = await getSolidDataset(url, { fetch: h2fetch }); + * h2fetch.close(); + * ``` + * + * @since 2.6.0 + */ +export function createHttp2Fetch( + options: { idleTimeout?: number; tlsOptions?: tls.ConnectionOptions } = {}, +): Http2Fetch { + const keepAliveTimeout = options.idleTimeout ?? DEFAULT_IDLE_TIMEOUT_MS; + + function createAgent(): Agent { + return new Agent({ + allowH2: true, + keepAliveTimeout, + connect: options.tlsOptions, + }); + } + + let agent = createAgent(); + + const h2fetch: typeof fetch = async ( + input: RequestInfo | URL, + init?: RequestInit, + ): Promise => { + return undiciFetch( + input as Parameters[0], + { ...init, dispatcher: agent } as Parameters[1], + ) as unknown as Promise; + }; + + function close(): void { + agent.close(); + agent = createAgent(); + } + + return Object.assign(h2fetch, { close }) as Http2Fetch; +} diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 266c9f4fab..379c2bf6ac 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -19,6 +19,8 @@ // export { Session, ISessionOptions } from "./Session"; +export { createHttp2Fetch } from "./http2Fetch"; +export type { Http2Fetch } from "./http2Fetch"; export { getSessionFromStorage, diff --git a/packages/node/src/login/oidc/OidcLoginHandler.ts b/packages/node/src/login/oidc/OidcLoginHandler.ts index c3bfc1a064..9d39064b65 100644 --- a/packages/node/src/login/oidc/OidcLoginHandler.ts +++ b/packages/node/src/login/oidc/OidcLoginHandler.ts @@ -125,6 +125,7 @@ export default class OidcLoginHandler implements ILoginHandler { eventEmitter: options.eventEmitter, keepAlive: options.keepAlive, scopes: normalizeScopes(options.customScopes), + customFetch: options.customFetch, }; // Call proper OIDC Handler return this.oidcHandler.handle(oidcOptions); diff --git a/packages/node/src/login/oidc/incomingRedirectHandler/AuthCodeRedirectHandler.ts b/packages/node/src/login/oidc/incomingRedirectHandler/AuthCodeRedirectHandler.ts index 6f1b41662f..d7696125d8 100644 --- a/packages/node/src/login/oidc/incomingRedirectHandler/AuthCodeRedirectHandler.ts +++ b/packages/node/src/login/oidc/incomingRedirectHandler/AuthCodeRedirectHandler.ts @@ -34,6 +34,7 @@ import type { RefreshOptions, ITokenRefresher, IncomingRedirectResult, + SessionConfig, } from "@inrupt/solid-client-authn-core"; import { loadOidcContextFromStorage, @@ -92,6 +93,7 @@ export class AuthCodeRedirectHandler implements IIncomingRedirectHandler { async handle( inputRedirectUrl: string, eventEmitter?: EventEmitter, + config?: SessionConfig, ): Promise { if (!(await this.canHandle(inputRedirectUrl))) { throw new Error( @@ -180,6 +182,7 @@ export class AuthCodeRedirectHandler implements IIncomingRedirectHandler { refreshOptions: oidcContext.keepAlive ? refreshOptions : undefined, eventEmitter, expiresIn: tokenSet.expires_in, + fetch: config?.customFetch, }); // tokenSet.claims() parses the ID token, validates its signature, and returns diff --git a/packages/node/src/login/oidc/oidcHandlers/ClientCredentialsOidcHandler.ts b/packages/node/src/login/oidc/oidcHandlers/ClientCredentialsOidcHandler.ts index 8af292eef7..bb1899d559 100644 --- a/packages/node/src/login/oidc/oidcHandlers/ClientCredentialsOidcHandler.ts +++ b/packages/node/src/login/oidc/oidcHandlers/ClientCredentialsOidcHandler.ts @@ -142,6 +142,7 @@ export default class ClientCredentialsOidcHandler implements IOidcHandler { : undefined, eventEmitter: oidcLoginOptions.eventEmitter, expiresIn: tokens.expires_in, + fetch: oidcLoginOptions.customFetch, }); const expiresAt = diff --git a/packages/node/src/login/oidc/oidcHandlers/RefreshTokenOidcHandler.ts b/packages/node/src/login/oidc/oidcHandlers/RefreshTokenOidcHandler.ts index 3a243be2d8..decab45bc3 100644 --- a/packages/node/src/login/oidc/oidcHandlers/RefreshTokenOidcHandler.ts +++ b/packages/node/src/login/oidc/oidcHandlers/RefreshTokenOidcHandler.ts @@ -74,6 +74,7 @@ async function refreshAccess( keepAlive: boolean, refreshBindingKey?: KeyPair, eventEmitter?: EventEmitter, + customFetch?: typeof fetch, ): Promise { try { let dpopKey: KeyPair | undefined; @@ -101,6 +102,7 @@ async function refreshAccess( refreshOptions: keepAlive ? rotatedRefreshOptions : undefined, eventEmitter, expiresIn: expiresInS > 0 ? expiresInS : undefined, + fetch: customFetch, }); return Object.assign(tokens, { fetch: authFetch, @@ -188,6 +190,7 @@ export default class RefreshTokenOidcHandler implements IOidcHandler { oidcLoginOptions.keepAlive ?? true, keyPair, oidcLoginOptions.eventEmitter, + oidcLoginOptions.customFetch, ); const sessionInfo: ISessionInfo = {