refactor(core): Migrate OIDC discovery fetch to outbound HTTP factory (no-changelog) (#32395)

This commit is contained in:
Lorent Lempereur 2026-06-18 13:43:27 +02:00 committed by GitHub
parent 1d6415cd59
commit ae17db7eb2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 235 additions and 370 deletions

View File

@ -1,5 +1,8 @@
import type { OidcConfigDto } from '@n8n/api-types';
import type { Logger } from '@n8n/backend-common';
import type { HttpTransport, SsrfProtectionService } from '@n8n/backend-network';
import { OutboundHttp } from '@n8n/backend-network';
import { type LocalServer, startServer } from '@n8n/backend-network/testing';
import { mockInstance, mockLogger } from '@n8n/backend-test-utils';
import type { GlobalConfig } from '@n8n/config';
import type { AuthIdentityRepository, SettingsRepository, User, UserRepository } from '@n8n/db';
@ -7,7 +10,6 @@ import { Container } from '@n8n/di';
import { mock } from 'jest-mock-extended';
import type { Cipher, InstanceSettings } from 'n8n-core';
import * as client from 'openid-client';
import { EnvHttpProxyAgent } from 'undici';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
@ -20,11 +22,6 @@ import * as ssoHelpers from '@/sso.ee/sso-helpers';
import { OIDC_PREFERENCES_DB_KEY } from '../constants';
import { OidcService } from '../oidc.service.ee';
jest.mock('undici', () => ({
// eslint-disable-next-line @typescript-eslint/naming-convention
EnvHttpProxyAgent: jest.fn().mockImplementation(() => ({})),
}));
describe('OidcService', () => {
let oidcService: OidcService;
let settingsRepository: SettingsRepository;
@ -36,6 +33,8 @@ describe('OidcService', () => {
let provisioningService: ProvisioningService;
let userRepository: UserRepository;
let authIdentityRepository: AuthIdentityRepository;
let outboundHttp: jest.Mocked<OutboundHttp>;
let customFetch: jest.Mock;
const mockOidcConfig = {
clientId: 'test-client-id',
@ -70,6 +69,11 @@ describe('OidcService', () => {
provisioningService = mock<ProvisioningService>();
userRepository = mock<UserRepository>();
authIdentityRepository = mock<AuthIdentityRepository>();
customFetch = jest.fn();
outboundHttp = mock<OutboundHttp>();
outboundHttp.transport.mockReturnValue(
mock<HttpTransport>({ asCustomFetch: () => customFetch }),
);
jest
.spyOn(ssoHelpers, 'setCurrentAuthenticationMethod')
.mockImplementation(async () => await Promise.resolve());
@ -85,6 +89,7 @@ describe('OidcService', () => {
jwtService,
instanceSettings,
provisioningService,
outboundHttp,
);
await oidcService.init();
@ -676,342 +681,119 @@ describe('OidcService', () => {
});
});
describe('proxy configuration', () => {
const originalEnv = process.env;
describe('createProxyAwareConfiguration', () => {
const discoveryUrl = new URL('https://example.com/.well-known/openid-configuration');
const clientId = 'test-client';
const clientSecret = 'test-secret';
// Helper function to create a proper mock Response
const createMockResponse = () => {
const mockData = {
issuer: 'https://example.com',
authorization_endpoint: 'https://example.com/auth',
token_endpoint: 'https://example.com/token',
userinfo_endpoint: 'https://example.com/userinfo',
jwks_uri: 'https://example.com/jwks',
};
return new Response(JSON.stringify(mockData), {
status: 200,
// eslint-disable-next-line @typescript-eslint/naming-convention
headers: { 'content-type': 'application/json' },
const createConfiguration = async () =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return
(await (oidcService as any).createProxyAwareConfiguration(
discoveryUrl,
clientId,
clientSecret,
)) as client.Configuration;
it("obtains the custom fetch from the factory transport with SSRF 'disabled'", async () => {
jest.spyOn(client, 'discovery').mockResolvedValue({} as client.Configuration);
await createConfiguration();
// The discovery / token / userinfo endpoints are admin-configured and may
// legitimately point at an internal IdP, so SSRF protection is disabled.
expect(outboundHttp.transport).toHaveBeenCalledWith({ ssrf: 'disabled' });
});
it('always calls discovery with the factory customFetch (no proxy/no-proxy branch)', async () => {
const discoverySpy = jest
.spyOn(client, 'discovery')
.mockResolvedValue({} as client.Configuration);
await createConfiguration();
expect(discoverySpy).toHaveBeenCalledWith(
discoveryUrl,
clientId,
clientSecret,
undefined,
expect.objectContaining({
[client.customFetch]: customFetch,
}),
);
});
it('sets the factory customFetch on the returned configuration', async () => {
jest.spyOn(client, 'discovery').mockResolvedValue({} as client.Configuration);
const result = await createConfiguration();
expect(result[client.customFetch]).toBe(customFetch);
});
});
// Exercises the customFetch produced by a real OutboundHttp against a real
// loopback server. openid-client drives discovery / token / userinfo through
// this fetch, so proving it performs a genuine HTTP round-trip (the same job
// the old hand-rolled proxyFetch did) validates the migrated behavior.
describe('factory customFetch (real HTTP round-trip)', () => {
let idpServer: LocalServer;
let realOidcService: OidcService;
beforeAll(async () => {
idpServer = await startServer((req, res) => {
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify({ ok: true, path: req.url }));
});
};
});
afterAll(async () => await idpServer.close());
beforeEach(() => {
// Reset environment before each test
process.env = { ...originalEnv };
// Reset the mock between tests
(EnvHttpProxyAgent as unknown as jest.Mock).mockClear();
});
afterEach(() => {
// Restore original environment after each test
process.env = originalEnv;
});
it.each([
{ envVar: 'HTTP_PROXY', value: 'http://proxy.example.com:8080' },
{ envVar: 'HTTPS_PROXY', value: 'https://proxy.example.com:8443' },
{ envVar: 'ALL_PROXY', value: 'http://all-proxy.example.com:8888' },
])('should instantiate EnvHttpProxyAgent when $envVar is set', async ({ envVar, value }) => {
// Set proxy environment variable
process.env[envVar] = value;
const discoveryUrl = new URL('https://example.com/.well-known/openid-configuration');
const clientId = 'test-client';
const clientSecret = 'test-secret';
const discoverySpy = jest.spyOn(client, 'discovery').mockResolvedValue({
serverMetadata: () => ({ issuer: 'https://example.com' }),
} as unknown as client.Configuration);
// Call the private method directly using type assertion
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
await (oidcService as any).createProxyAwareConfiguration(
discoveryUrl,
clientId,
clientSecret,
idpServer.clear();
const realOutboundHttp = new OutboundHttp(mock<SsrfProtectionService>(), logger);
realOidcService = new OidcService(
settingsRepository,
authIdentityRepository,
mock<UrlService>(),
globalConfig,
userRepository,
cipher,
logger,
jwtService,
instanceSettings,
provisioningService,
realOutboundHttp,
);
// Verify EnvHttpProxyAgent was instantiated
expect(EnvHttpProxyAgent).toHaveBeenCalled();
discoverySpy.mockRestore();
});
it('should not instantiate EnvHttpProxyAgent when no proxy env vars are set', async () => {
// Ensure no proxy env vars are set
delete process.env.HTTP_PROXY;
delete process.env.HTTPS_PROXY;
delete process.env.ALL_PROXY;
const discoveryUrl = new URL('https://example.com/.well-known/openid-configuration');
const clientId = 'test-client';
const clientSecret = 'test-secret';
const discoverySpy = jest.spyOn(client, 'discovery').mockResolvedValue({
serverMetadata: () => ({ issuer: 'https://example.com' }),
} as unknown as client.Configuration);
// Call the private method directly
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
await (oidcService as any).createProxyAwareConfiguration(
discoveryUrl,
clientId,
clientSecret,
);
// Should not instantiate EnvHttpProxyAgent when no proxy is configured
expect(EnvHttpProxyAgent).not.toHaveBeenCalled();
discoverySpy.mockRestore();
});
it.each([
{ envVar: 'HTTP_PROXY', value: 'http://proxy.example.com:8080' },
{ envVar: 'HTTPS_PROXY', value: 'https://proxy.example.com:8443' },
{ envVar: 'ALL_PROXY', value: 'http://all-proxy.example.com:8888' },
])(
'should call discovery with customFetch option when $envVar is configured',
async ({ envVar, value }) => {
process.env[envVar] = value;
const discoveryUrl = new URL('https://example.com/.well-known/openid-configuration');
const clientId = 'test-client';
const clientSecret = 'test-secret';
global.fetch = jest.fn().mockResolvedValue(createMockResponse());
const discoverySpy = jest.spyOn(client, 'discovery').mockResolvedValue({
serverMetadata: () => ({ issuer: 'https://example.com' }),
} as unknown as client.Configuration);
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
await (oidcService as any).createProxyAwareConfiguration(
discoveryUrl,
clientId,
clientSecret,
);
expect(discoverySpy).toHaveBeenCalledWith(
discoveryUrl,
clientId,
clientSecret,
undefined,
expect.objectContaining({
[client.customFetch]: expect.any(Function),
}),
);
discoverySpy.mockRestore();
},
);
it('should call discovery without customFetch option when no proxy is configured', async () => {
delete process.env.HTTP_PROXY;
delete process.env.HTTPS_PROXY;
delete process.env.ALL_PROXY;
const discoveryUrl = new URL('https://example.com/.well-known/openid-configuration');
const clientId = 'test-client';
const clientSecret = 'test-secret';
global.fetch = jest.fn().mockResolvedValue(createMockResponse());
const discoverySpy = jest.spyOn(client, 'discovery').mockResolvedValue({
serverMetadata: () => ({ issuer: 'https://example.com' }),
} as unknown as client.Configuration);
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
await (oidcService as any).createProxyAwareConfiguration(
discoveryUrl,
clientId,
clientSecret,
);
// Should be called with only 3 arguments (no options object)
expect(discoverySpy).toHaveBeenCalledWith(discoveryUrl, clientId, clientSecret);
discoverySpy.mockRestore();
});
it.each([
{ envVar: 'HTTP_PROXY', value: 'http://proxy.example.com:8080' },
{ envVar: 'HTTPS_PROXY', value: 'https://proxy.example.com:8443' },
{ envVar: 'ALL_PROXY', value: 'http://all-proxy.example.com:8888' },
])(
'should set customFetch on returned configuration when $envVar is configured',
async ({ envVar, value }) => {
process.env[envVar] = value;
const discoveryUrl = new URL('https://example.com/.well-known/openid-configuration');
const clientId = 'test-client';
const clientSecret = 'test-secret';
global.fetch = jest.fn().mockResolvedValue(createMockResponse());
const mockConfiguration = {
serverMetadata: () => ({ issuer: 'https://example.com' }),
} as unknown as client.Configuration;
jest.spyOn(client, 'discovery').mockResolvedValue(mockConfiguration);
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
const result = await (oidcService as any).createProxyAwareConfiguration(
discoveryUrl,
clientId,
clientSecret,
);
// Verify customFetch was set on the configuration
expect(result[client.customFetch]).toBeDefined();
expect(typeof result[client.customFetch]).toBe('function');
},
);
it('should not set customFetch on returned configuration when no proxy is configured', async () => {
delete process.env.HTTP_PROXY;
delete process.env.HTTPS_PROXY;
delete process.env.ALL_PROXY;
const discoveryUrl = new URL('https://example.com/.well-known/openid-configuration');
const clientId = 'test-client';
const clientSecret = 'test-secret';
global.fetch = jest.fn().mockResolvedValue(createMockResponse());
const mockConfiguration = {
serverMetadata: () => ({ issuer: 'https://example.com' }),
} as unknown as client.Configuration;
jest.spyOn(client, 'discovery').mockResolvedValue(mockConfiguration);
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
const result = await (oidcService as any).createProxyAwareConfiguration(
discoveryUrl,
clientId,
clientSecret,
);
// customFetch should not have been set on the configuration
expect(result[client.customFetch]).toBeUndefined();
});
it.each([
{ envVar: 'HTTP_PROXY', value: 'http://proxy.example.com:8080' },
{ envVar: 'HTTPS_PROXY', value: 'https://proxy.example.com:8443' },
{ envVar: 'ALL_PROXY', value: 'http://all-proxy.example.com:8888' },
])(
'should use proxy agent dispatcher in customFetch when $envVar is configured',
async ({ envVar, value }) => {
process.env[envVar] = value;
const discoveryUrl = new URL('https://example.com/.well-known/openid-configuration');
const clientId = 'test-client';
const clientSecret = 'test-secret';
const mockProxyAgent = { type: 'proxy-agent' };
(EnvHttpProxyAgent as unknown as jest.Mock).mockImplementation(() => mockProxyAgent);
const fetchSpy = jest.fn().mockResolvedValue(createMockResponse());
global.fetch = fetchSpy;
const mockConfiguration = {
serverMetadata: () => ({ issuer: 'https://example.com' }),
} as unknown as client.Configuration;
jest.spyOn(client, 'discovery').mockResolvedValue(mockConfiguration);
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
const result = await (oidcService as any).createProxyAwareConfiguration(
discoveryUrl,
clientId,
clientSecret,
);
// Get the customFetch function and call it
const customFetch = result[client.customFetch];
const testUrl = 'https://example.com/test';
const testOptions = { method: 'GET' };
await customFetch(testUrl, testOptions);
// Verify fetch was called with dispatcher option
expect(fetchSpy).toHaveBeenCalledWith(testUrl, {
...testOptions,
dispatcher: mockProxyAgent,
it('routes openid-client fetches through a real HTTP socket', async () => {
let factoryFetch: client.CustomFetch | undefined;
jest
.spyOn(client, 'discovery')
.mockImplementation(async (_server, _clientId, _metadata, _auth, options) => {
factoryFetch = options?.[client.customFetch];
return {} as client.Configuration;
});
},
);
it('should handle multiple proxy env vars with priority (first match)', async () => {
// Set multiple proxy variables - any of them should trigger proxy mode
process.env.HTTP_PROXY = 'http://http-proxy.example.com:8080';
process.env.HTTPS_PROXY = 'https://https-proxy.example.com:8443';
const discoveryUrl = new URL('https://example.com/.well-known/openid-configuration');
const clientId = 'test-client';
const clientSecret = 'test-secret';
const discoverySpy = jest.spyOn(client, 'discovery').mockResolvedValue({
serverMetadata: () => ({ issuer: 'https://example.com' }),
} as unknown as client.Configuration);
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
await (oidcService as any).createProxyAwareConfiguration(
discoveryUrl,
clientId,
clientSecret,
await (realOidcService as any).createProxyAwareConfiguration(
new URL('https://issuer.example.com/.well-known/openid-configuration'),
'real-client',
'real-secret',
);
// EnvHttpProxyAgent should be instantiated once regardless of how many proxy vars are set
expect(EnvHttpProxyAgent).toHaveBeenCalledTimes(1);
discoverySpy.mockRestore();
expect(factoryFetch).toBeDefined();
const response = await factoryFetch!(`${idpServer.url}/userinfo`, {
method: 'GET',
headers: {},
body: null,
redirect: 'manual',
});
const body = (await response.json()) as { ok: boolean; path: string };
expect(idpServer.captured).toEqual(['/userinfo']);
expect(body).toEqual({ ok: true, path: '/userinfo' });
});
it.each([
{ envVar: 'HTTP_PROXY', value: 'http://proxy.example.com:8080' },
{ envVar: 'HTTPS_PROXY', value: 'https://proxy.example.com:8443' },
{ envVar: 'ALL_PROXY', value: 'http://all-proxy.example.com:8888' },
])(
'should pass through fetch options correctly when $envVar is configured',
async ({ envVar, value }) => {
process.env[envVar] = value;
const discoveryUrl = new URL('https://example.com/.well-known/openid-configuration');
const clientId = 'test-client';
const clientSecret = 'test-secret';
const mockProxyAgent = { type: 'proxy-agent' };
(EnvHttpProxyAgent as unknown as jest.Mock).mockImplementation(() => mockProxyAgent);
const fetchSpy = jest.fn().mockResolvedValue(createMockResponse());
global.fetch = fetchSpy;
const mockConfiguration = {} as client.Configuration;
jest.spyOn(client, 'discovery').mockResolvedValue(mockConfiguration);
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
const result = await (oidcService as any).createProxyAwareConfiguration(
discoveryUrl,
clientId,
clientSecret,
);
const customFetch = result[client.customFetch];
const testUrl = 'https://example.com/token';
const testOptions = {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'grant_type=authorization_code&code=test',
};
await customFetch(testUrl, testOptions);
// Verify all original options are preserved and dispatcher is added
expect(fetchSpy).toHaveBeenCalledWith(testUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'grant_type=authorization_code&code=test',
dispatcher: mockProxyAgent,
});
},
);
});
});

View File

@ -1,5 +1,6 @@
import { OidcConfigDto } from '@n8n/api-types';
import { Logger } from '@n8n/backend-common';
import { OutboundHttp } from '@n8n/backend-network';
import { GlobalConfig } from '@n8n/config';
import {
AuthIdentity,
@ -16,7 +17,6 @@ import { randomUUID } from 'crypto';
import { Cipher, InstanceSettings } from 'n8n-core';
import { jsonParse, UserError } from 'n8n-workflow';
import type * as openidClientTypes from 'openid-client';
import { EnvHttpProxyAgent } from 'undici';
import { inspect } from 'util';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
@ -99,6 +99,7 @@ export class OidcService {
private readonly jwtService: JwtService,
private readonly instanceSettings: InstanceSettings,
private readonly provisioningService: ProvisioningService,
private readonly outboundHttp: OutboundHttp,
) {}
async init() {
@ -695,8 +696,7 @@ export class OidcService {
| undefined;
/**
* Creates a proxy-aware configuration for openid-client.
* This method configures customFetch to respect HTTP_PROXY, HTTPS_PROXY, and NO_PROXY environment variables.
* Creates a configuration for openid-client whose HTTP calls route through the outbound HTTP factory.
*/
private async createProxyAwareConfiguration(
discoveryUrl: URL,
@ -705,46 +705,31 @@ export class OidcService {
): Promise<openidClientTypes.Configuration> {
await this.loadOpenIdClient();
// Check if proxy environment variables are set
const hasProxyConfig =
process.env.HTTP_PROXY ?? process.env.HTTPS_PROXY ?? process.env.ALL_PROXY;
const customFetch = this.outboundHttp
.transport({
// SSRF is explicitly disabled: the discovery endpoint (and the issuer's
// token/userinfo endpoints reached with the same `customFetch`) is
// admin-configured and may legitimately point at an internal IdP, so enabling
// SSRF protection here would block valid internal setups
ssrf: 'disabled',
// `proxy` defaults = `'env'`
})
.asCustomFetch() as unknown as openidClientTypes.CustomFetch;
if (hasProxyConfig) {
this.logger.debug('Configuring OIDC client with proxy support', {
HTTP_PROXY: process.env.HTTP_PROXY,
HTTPS_PROXY: process.env.HTTPS_PROXY,
NO_PROXY: process.env.NO_PROXY,
ALL_PROXY: process.env.ALL_PROXY,
});
const configuration = await this.openidClient.discovery(
discoveryUrl,
clientId,
clientSecret,
undefined,
{
[this.openidClient.customFetch]: customFetch,
},
);
// Create a proxy agent that automatically reads from environment variables
const proxyAgent = new EnvHttpProxyAgent();
const proxyFetch: openidClientTypes.CustomFetch = async (url, options) => {
return await fetch(url, {
...options,
// @ts-expect-error - dispatcher is an undici-specific option not in standard fetch
dispatcher: proxyAgent,
});
};
// Reuse the same fetch for token-exchange / userinfo on the returned configuration.
configuration[this.openidClient.customFetch] = customFetch;
// discovery call with custom fetch client using proxy agent
const configuration = await this.openidClient.discovery(
discoveryUrl,
clientId,
clientSecret,
undefined,
{
[this.openidClient.customFetch]: proxyFetch,
},
);
// Configure customFetch to use the proxy agent
configuration[this.openidClient.customFetch] = proxyFetch;
return configuration;
}
return await this.openidClient.discovery(discoveryUrl, clientId, clientSecret);
return configuration;
}
private async getOidcConfiguration(): Promise<openidClientTypes.Configuration> {

View File

@ -0,0 +1,90 @@
import type { Logger } from '@n8n/backend-common';
import { OutboundHttp, type SsrfProtectionService } from '@n8n/backend-network';
import { type LocalServer, startServer } from '@n8n/backend-network/testing';
import { mock } from 'jest-mock-extended';
import * as client from 'openid-client';
describe('OIDC discovery (real HTTP round-trip through the factory)', () => {
let idpServer: LocalServer;
const buildCustomFetch = () => {
const outboundHttp = new OutboundHttp(mock<SsrfProtectionService>(), mock<Logger>());
return outboundHttp
.transport({ ssrf: 'disabled' })
.asCustomFetch() as unknown as client.CustomFetch;
};
const discoveryDocument = (origin: string) => ({
issuer: origin,
authorization_endpoint: `${origin}/auth`,
token_endpoint: `${origin}/token`,
userinfo_endpoint: `${origin}/userinfo`,
jwks_uri: `${origin}/jwks`,
response_types_supported: ['code'],
subject_types_supported: ['public'],
id_token_signing_alg_values_supported: ['RS256'],
});
beforeAll(async () => {
idpServer = await startServer((_req, res) => {
// eslint-disable-next-line @typescript-eslint/naming-convention
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify(discoveryDocument(idpServer.url)));
});
});
afterAll(async () => await idpServer.close());
beforeEach(() => idpServer.clear());
it('fetches and parses the discovery document over a real socket', async () => {
const customFetch = buildCustomFetch();
const configuration = await client.discovery(
new URL(`${idpServer.url}/.well-known/openid-configuration`),
'real-client',
'real-secret',
undefined,
{
execute: [client.allowInsecureRequests],
[client.customFetch]: customFetch,
},
);
// The request actually went out over the wire ...
expect(idpServer.captured).toEqual(['/.well-known/openid-configuration']);
// ... and openid-client parsed the served metadata into a Configuration.
expect(configuration.serverMetadata().issuer).toBe(idpServer.url);
expect(configuration.serverMetadata().token_endpoint).toBe(`${idpServer.url}/token`);
});
it('reuses the same customFetch on the configuration for subsequent real requests', async () => {
const customFetch = buildCustomFetch();
const configuration = await client.discovery(
new URL(`${idpServer.url}/.well-known/openid-configuration`),
'real-client',
'real-secret',
undefined,
{
execute: [client.allowInsecureRequests],
[client.customFetch]: customFetch,
},
);
// The service sets the same fetch on the returned configuration so that
// token-exchange / userinfo route through the factory too.
configuration[client.customFetch] = customFetch;
idpServer.clear();
// Drive that carried fetch directly against a real endpoint.
const response = await configuration[client.customFetch](`${idpServer.url}/token`, {
method: 'GET',
headers: {},
body: null,
redirect: 'manual',
});
expect(response.status).toBe(200);
expect(idpServer.captured).toEqual(['/token']);
});
});

View File

@ -50,6 +50,10 @@ describe('OIDC service', () => {
});
describe('loadConfig', () => {
beforeEach(() => {
discoveryMock.mockResolvedValue({});
});
it('should initialize with default config', () => {
expect(oidcService.getRedactedConfig()).toEqual({
clientId: '',
@ -178,6 +182,10 @@ describe('OIDC service', () => {
expect.any(URL),
'test-client-id',
'test-client-secret',
undefined,
expect.objectContaining({
[real_odic_client.customFetch]: expect.any(Function),
}),
);
});