tududi/backend/tests/unit/modules/oidc/providerConfig.test.js
Chris 93bcbc0485
fix(oidc): normalize OIDC_SCOPE to handle whitespace issues (#1060)
Adds automatic scope normalization to prevent URL encoding issues when
OIDC_SCOPE contains extra whitespace, tabs, or newlines. This addresses
issue #1056 where spaces in the scope value could cause authentication
failures in some environments.

Changes:
- Added normalizeScope() function to trim and collapse whitespace
- Automatically adds 'openid' scope if missing with warning
- Updated both single and multi-provider configurations
- Added comprehensive tests for scope normalization edge cases
- Added service tests to verify authorization URL construction
- Updated documentation with scope formatting guidance

Fixes #1056
2026-04-24 13:15:58 +03:00

424 lines
17 KiB
JavaScript

const providerConfig = require('../../../../modules/oidc/providerConfig');
describe('OIDC Provider Configuration', () => {
let originalEnv;
beforeEach(() => {
originalEnv = { ...process.env };
providerConfig.reloadProviders();
});
afterEach(() => {
process.env = originalEnv;
providerConfig.reloadProviders();
});
describe('when OIDC is disabled', () => {
it('should return empty array when OIDC_ENABLED is not true', () => {
process.env.OIDC_ENABLED = 'false';
providerConfig.reloadProviders();
const providers = providerConfig.getAllProviders();
expect(providers).toEqual([]);
expect(providerConfig.isOidcEnabled()).toBe(false);
});
it('should return empty array when OIDC_ENABLED is not set', () => {
delete process.env.OIDC_ENABLED;
providerConfig.reloadProviders();
const providers = providerConfig.getAllProviders();
expect(providers).toEqual([]);
expect(providerConfig.isOidcEnabled()).toBe(false);
});
});
describe('single provider configuration', () => {
it('should load single provider from .env', () => {
process.env.OIDC_ENABLED = 'true';
process.env.OIDC_PROVIDER_NAME = 'Google';
process.env.OIDC_PROVIDER_SLUG = 'google';
process.env.OIDC_ISSUER_URL = 'https://accounts.google.com';
process.env.OIDC_CLIENT_ID = 'test-client-id';
process.env.OIDC_CLIENT_SECRET = 'test-client-secret';
providerConfig.reloadProviders();
const providers = providerConfig.getAllProviders();
expect(providers).toHaveLength(1);
expect(providers[0]).toMatchObject({
slug: 'google',
name: 'Google',
issuer: 'https://accounts.google.com',
clientId: 'test-client-id',
clientSecret: 'test-client-secret',
scope: 'openid profile email',
autoProvision: true,
});
});
it('should use default slug if not provided', () => {
process.env.OIDC_ENABLED = 'true';
process.env.OIDC_PROVIDER_NAME = 'Custom Provider';
process.env.OIDC_ISSUER_URL = 'https://auth.example.com';
process.env.OIDC_CLIENT_ID = 'test-id';
process.env.OIDC_CLIENT_SECRET = 'test-secret';
providerConfig.reloadProviders();
const provider = providerConfig.getProvider('default');
expect(provider).toBeDefined();
expect(provider.slug).toBe('default');
});
it('should parse custom scope', () => {
process.env.OIDC_ENABLED = 'true';
process.env.OIDC_PROVIDER_NAME = 'Okta';
process.env.OIDC_PROVIDER_SLUG = 'okta';
process.env.OIDC_ISSUER_URL = 'https://company.okta.com';
process.env.OIDC_CLIENT_ID = 'test-id';
process.env.OIDC_CLIENT_SECRET = 'test-secret';
process.env.OIDC_SCOPE = 'openid profile email groups';
providerConfig.reloadProviders();
const provider = providerConfig.getProvider('okta');
expect(provider.scope).toBe('openid profile email groups');
});
it('should normalize scope with extra whitespace', () => {
process.env.OIDC_ENABLED = 'true';
process.env.OIDC_PROVIDER_NAME = 'Test';
process.env.OIDC_PROVIDER_SLUG = 'test';
process.env.OIDC_ISSUER_URL = 'https://auth.example.com';
process.env.OIDC_CLIENT_ID = 'test-id';
process.env.OIDC_CLIENT_SECRET = 'test-secret';
process.env.OIDC_SCOPE = ' openid profile email ';
providerConfig.reloadProviders();
const provider = providerConfig.getProvider('test');
expect(provider.scope).toBe('openid profile email');
});
it('should add openid scope if missing', () => {
const consoleWarnSpy = jest
.spyOn(console, 'warn')
.mockImplementation();
process.env.OIDC_ENABLED = 'true';
process.env.OIDC_PROVIDER_NAME = 'Test';
process.env.OIDC_PROVIDER_SLUG = 'test';
process.env.OIDC_ISSUER_URL = 'https://auth.example.com';
process.env.OIDC_CLIENT_ID = 'test-id';
process.env.OIDC_CLIENT_SECRET = 'test-secret';
process.env.OIDC_SCOPE = 'profile email';
providerConfig.reloadProviders();
const provider = providerConfig.getProvider('test');
expect(provider.scope).toBe('openid profile email');
expect(consoleWarnSpy).toHaveBeenCalledWith(
expect.stringContaining("does not include 'openid'")
);
consoleWarnSpy.mockRestore();
});
it('should handle scope with tabs and newlines', () => {
process.env.OIDC_ENABLED = 'true';
process.env.OIDC_PROVIDER_NAME = 'Test';
process.env.OIDC_PROVIDER_SLUG = 'test';
process.env.OIDC_ISSUER_URL = 'https://auth.example.com';
process.env.OIDC_CLIENT_ID = 'test-id';
process.env.OIDC_CLIENT_SECRET = 'test-secret';
process.env.OIDC_SCOPE = 'openid\tprofile\nemail';
providerConfig.reloadProviders();
const provider = providerConfig.getProvider('test');
expect(provider.scope).toBe('openid profile email');
});
it('should parse admin email domains', () => {
process.env.OIDC_ENABLED = 'true';
process.env.OIDC_PROVIDER_NAME = 'Google';
process.env.OIDC_PROVIDER_SLUG = 'google';
process.env.OIDC_ISSUER_URL = 'https://accounts.google.com';
process.env.OIDC_CLIENT_ID = 'test-id';
process.env.OIDC_CLIENT_SECRET = 'test-secret';
process.env.OIDC_ADMIN_EMAIL_DOMAINS = 'example.com,company.com';
providerConfig.reloadProviders();
const provider = providerConfig.getProvider('google');
expect(provider.adminEmailDomains).toEqual([
'example.com',
'company.com',
]);
});
it('should respect AUTO_PROVISION=false', () => {
process.env.OIDC_ENABLED = 'true';
process.env.OIDC_PROVIDER_NAME = 'Okta';
process.env.OIDC_PROVIDER_SLUG = 'okta';
process.env.OIDC_ISSUER_URL = 'https://company.okta.com';
process.env.OIDC_CLIENT_ID = 'test-id';
process.env.OIDC_CLIENT_SECRET = 'test-secret';
process.env.OIDC_AUTO_PROVISION = 'false';
providerConfig.reloadProviders();
const provider = providerConfig.getProvider('okta');
expect(provider.autoProvision).toBe(false);
});
it('should return empty array if configuration is incomplete', () => {
const consoleErrorSpy = jest
.spyOn(console, 'error')
.mockImplementation();
process.env.OIDC_ENABLED = 'true';
process.env.OIDC_PROVIDER_NAME = 'Google';
providerConfig.reloadProviders();
const providers = providerConfig.getAllProviders();
expect(providers).toEqual([]);
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('Cannot load OIDC provider')
);
consoleErrorSpy.mockRestore();
});
});
describe('multiple provider configuration', () => {
it('should load multiple numbered providers', () => {
process.env.OIDC_ENABLED = 'true';
process.env.OIDC_PROVIDER_1_NAME = 'Google';
process.env.OIDC_PROVIDER_1_SLUG = 'google';
process.env.OIDC_PROVIDER_1_ISSUER = 'https://accounts.google.com';
process.env.OIDC_PROVIDER_1_CLIENT_ID = 'google-id';
process.env.OIDC_PROVIDER_1_CLIENT_SECRET = 'google-secret';
process.env.OIDC_PROVIDER_2_NAME = 'Okta';
process.env.OIDC_PROVIDER_2_SLUG = 'okta';
process.env.OIDC_PROVIDER_2_ISSUER = 'https://company.okta.com';
process.env.OIDC_PROVIDER_2_CLIENT_ID = 'okta-id';
process.env.OIDC_PROVIDER_2_CLIENT_SECRET = 'okta-secret';
providerConfig.reloadProviders();
const providers = providerConfig.getAllProviders();
expect(providers).toHaveLength(2);
expect(providers[0].slug).toBe('google');
expect(providers[1].slug).toBe('okta');
});
it('should skip numbered providers with incomplete config', () => {
const consoleWarnSpy = jest
.spyOn(console, 'warn')
.mockImplementation();
const consoleLogSpy = jest
.spyOn(console, 'log')
.mockImplementation();
process.env.OIDC_ENABLED = 'true';
process.env.OIDC_PROVIDER_1_NAME = 'Google';
process.env.OIDC_PROVIDER_1_SLUG = 'google';
process.env.OIDC_PROVIDER_2_NAME = 'Okta';
process.env.OIDC_PROVIDER_2_SLUG = 'okta';
process.env.OIDC_PROVIDER_2_ISSUER = 'https://company.okta.com';
process.env.OIDC_PROVIDER_2_CLIENT_ID = 'okta-id';
process.env.OIDC_PROVIDER_2_CLIENT_SECRET = 'okta-secret';
providerConfig.reloadProviders();
const providers = providerConfig.getAllProviders();
expect(providers).toHaveLength(1);
expect(providers[0].slug).toBe('okta');
expect(consoleWarnSpy).toHaveBeenCalledWith(
expect.stringContaining('Skipping OIDC provider 1')
);
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('Loaded OIDC provider 2: Okta')
);
consoleWarnSpy.mockRestore();
consoleLogSpy.mockRestore();
});
it('should handle different settings per provider', () => {
process.env.OIDC_ENABLED = 'true';
process.env.OIDC_PROVIDER_1_NAME = 'Google';
process.env.OIDC_PROVIDER_1_SLUG = 'google';
process.env.OIDC_PROVIDER_1_ISSUER = 'https://accounts.google.com';
process.env.OIDC_PROVIDER_1_CLIENT_ID = 'google-id';
process.env.OIDC_PROVIDER_1_CLIENT_SECRET = 'google-secret';
process.env.OIDC_PROVIDER_1_AUTO_PROVISION = 'true';
process.env.OIDC_PROVIDER_2_NAME = 'Corporate';
process.env.OIDC_PROVIDER_2_SLUG = 'corp';
process.env.OIDC_PROVIDER_2_ISSUER = 'https://auth.corp.com';
process.env.OIDC_PROVIDER_2_CLIENT_ID = 'corp-id';
process.env.OIDC_PROVIDER_2_CLIENT_SECRET = 'corp-secret';
process.env.OIDC_PROVIDER_2_AUTO_PROVISION = 'false';
process.env.OIDC_PROVIDER_2_ADMIN_EMAIL_DOMAINS = 'corp.com';
providerConfig.reloadProviders();
const google = providerConfig.getProvider('google');
const corp = providerConfig.getProvider('corp');
expect(google.autoProvision).toBe(true);
expect(google.adminEmailDomains).toEqual([]);
expect(corp.autoProvision).toBe(false);
expect(corp.adminEmailDomains).toEqual(['corp.com']);
});
it('should normalize scopes in multi-provider configuration', () => {
process.env.OIDC_ENABLED = 'true';
process.env.OIDC_PROVIDER_1_NAME = 'Google';
process.env.OIDC_PROVIDER_1_SLUG = 'google';
process.env.OIDC_PROVIDER_1_ISSUER = 'https://accounts.google.com';
process.env.OIDC_PROVIDER_1_CLIENT_ID = 'google-id';
process.env.OIDC_PROVIDER_1_CLIENT_SECRET = 'google-secret';
process.env.OIDC_PROVIDER_1_SCOPE = ' openid profile email ';
process.env.OIDC_PROVIDER_2_NAME = 'Okta';
process.env.OIDC_PROVIDER_2_SLUG = 'okta';
process.env.OIDC_PROVIDER_2_ISSUER = 'https://company.okta.com';
process.env.OIDC_PROVIDER_2_CLIENT_ID = 'okta-id';
process.env.OIDC_PROVIDER_2_CLIENT_SECRET = 'okta-secret';
process.env.OIDC_PROVIDER_2_SCOPE = 'openid profile email groups';
providerConfig.reloadProviders();
const google = providerConfig.getProvider('google');
const okta = providerConfig.getProvider('okta');
expect(google.scope).toBe('openid profile email');
expect(okta.scope).toBe('openid profile email groups');
});
});
describe('getProvider', () => {
beforeEach(() => {
process.env.OIDC_ENABLED = 'true';
process.env.OIDC_PROVIDER_NAME = 'Google';
process.env.OIDC_PROVIDER_SLUG = 'google';
process.env.OIDC_ISSUER_URL = 'https://accounts.google.com';
process.env.OIDC_CLIENT_ID = 'test-id';
process.env.OIDC_CLIENT_SECRET = 'test-secret';
providerConfig.reloadProviders();
});
it('should return provider by slug', () => {
const provider = providerConfig.getProvider('google');
expect(provider).toBeDefined();
expect(provider.slug).toBe('google');
});
it('should return null for non-existent slug', () => {
const provider = providerConfig.getProvider('nonexistent');
expect(provider).toBeNull();
});
});
describe('isOidcEnabled', () => {
it('should return true when OIDC is enabled with valid provider', () => {
const consoleLogSpy = jest
.spyOn(console, 'log')
.mockImplementation();
process.env.OIDC_ENABLED = 'true';
process.env.OIDC_PROVIDER_NAME = 'Google';
process.env.OIDC_PROVIDER_SLUG = 'google';
process.env.OIDC_ISSUER_URL = 'https://accounts.google.com';
process.env.OIDC_CLIENT_ID = 'test-id';
process.env.OIDC_CLIENT_SECRET = 'test-secret';
providerConfig.reloadProviders();
expect(providerConfig.isOidcEnabled()).toBe(true);
consoleLogSpy.mockRestore();
});
it('should return false when OIDC_ENABLED is false', () => {
const consoleLogSpy = jest
.spyOn(console, 'log')
.mockImplementation();
process.env.OIDC_ENABLED = 'false';
providerConfig.reloadProviders();
expect(providerConfig.isOidcEnabled()).toBe(false);
consoleLogSpy.mockRestore();
});
it('should return false when no providers configured', () => {
const consoleWarnSpy = jest
.spyOn(console, 'warn')
.mockImplementation();
process.env.OIDC_ENABLED = 'true';
providerConfig.reloadProviders();
expect(providerConfig.isOidcEnabled()).toBe(false);
expect(consoleWarnSpy).toHaveBeenCalledWith(
expect.stringContaining(
'OIDC is enabled but no valid providers are configured'
)
);
consoleWarnSpy.mockRestore();
});
});
describe('provider caching', () => {
it('should cache providers after first load', () => {
process.env.OIDC_ENABLED = 'true';
process.env.OIDC_PROVIDER_NAME = 'Google';
process.env.OIDC_PROVIDER_SLUG = 'google';
process.env.OIDC_ISSUER_URL = 'https://accounts.google.com';
process.env.OIDC_CLIENT_ID = 'test-id';
process.env.OIDC_CLIENT_SECRET = 'test-secret';
providerConfig.reloadProviders();
const providers1 = providerConfig.getAllProviders();
process.env.OIDC_PROVIDER_NAME = 'Changed';
const providers2 = providerConfig.getAllProviders();
expect(providers1).toBe(providers2);
expect(providers2[0].name).toBe('Google');
});
it('should reload providers when reloadProviders is called', () => {
process.env.OIDC_ENABLED = 'true';
process.env.OIDC_PROVIDER_NAME = 'Google';
process.env.OIDC_PROVIDER_SLUG = 'google';
process.env.OIDC_ISSUER_URL = 'https://accounts.google.com';
process.env.OIDC_CLIENT_ID = 'test-id';
process.env.OIDC_CLIENT_SECRET = 'test-secret';
providerConfig.reloadProviders();
const providers1 = providerConfig.getAllProviders();
process.env.OIDC_PROVIDER_NAME = 'Changed';
providerConfig.reloadProviders();
const providers2 = providerConfig.getAllProviders();
expect(providers1).not.toBe(providers2);
expect(providers2[0].name).toBe('Changed');
});
});
});