tududi/backend/modules/oidc/providerConfig.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

145 lines
4.4 KiB
JavaScript

function parseCommaSeparated(value) {
if (!value) return [];
return value
.split(',')
.map((v) => v.trim())
.filter(Boolean);
}
function normalizeScope(scope) {
if (!scope) return 'openid profile email';
const normalized = scope.trim().split(/\s+/).filter(Boolean).join(' ');
if (!normalized.includes('openid')) {
console.warn(
`OIDC scope does not include 'openid'. Adding it automatically. Original scope: "${scope}"`
);
return `openid ${normalized}`;
}
return normalized;
}
function loadProvidersFromEnv() {
if (process.env.OIDC_ENABLED !== 'true') {
console.log(
'OIDC is disabled. Set OIDC_ENABLED=true to enable SSO authentication.'
);
return [];
}
const providers = [];
let i = 1;
while (process.env[`OIDC_PROVIDER_${i}_NAME`]) {
const provider = {
slug: process.env[`OIDC_PROVIDER_${i}_SLUG`],
name: process.env[`OIDC_PROVIDER_${i}_NAME`],
issuer: process.env[`OIDC_PROVIDER_${i}_ISSUER`],
clientId: process.env[`OIDC_PROVIDER_${i}_CLIENT_ID`],
clientSecret: process.env[`OIDC_PROVIDER_${i}_CLIENT_SECRET`],
scope: normalizeScope(process.env[`OIDC_PROVIDER_${i}_SCOPE`]),
autoProvision:
process.env[`OIDC_PROVIDER_${i}_AUTO_PROVISION`] !== 'false',
adminEmailDomains: parseCommaSeparated(
process.env[`OIDC_PROVIDER_${i}_ADMIN_EMAIL_DOMAINS`]
),
};
const missingFields = [];
if (!provider.slug) missingFields.push(`OIDC_PROVIDER_${i}_SLUG`);
if (!provider.name) missingFields.push(`OIDC_PROVIDER_${i}_NAME`);
if (!provider.issuer) missingFields.push(`OIDC_PROVIDER_${i}_ISSUER`);
if (!provider.clientId)
missingFields.push(`OIDC_PROVIDER_${i}_CLIENT_ID`);
if (!provider.clientSecret)
missingFields.push(`OIDC_PROVIDER_${i}_CLIENT_SECRET`);
if (missingFields.length > 0) {
console.warn(
`Skipping OIDC provider ${i} due to missing required fields: ${missingFields.join(', ')}`
);
i++;
continue;
}
console.log(`Loaded OIDC provider ${i}: ${provider.name}`);
providers.push(provider);
i++;
}
if (providers.length === 0 && process.env.OIDC_PROVIDER_NAME) {
const provider = {
slug: process.env.OIDC_PROVIDER_SLUG || 'default',
name: process.env.OIDC_PROVIDER_NAME,
issuer: process.env.OIDC_ISSUER_URL,
clientId: process.env.OIDC_CLIENT_ID,
clientSecret: process.env.OIDC_CLIENT_SECRET,
scope: normalizeScope(process.env.OIDC_SCOPE),
autoProvision: process.env.OIDC_AUTO_PROVISION !== 'false',
adminEmailDomains: parseCommaSeparated(
process.env.OIDC_ADMIN_EMAIL_DOMAINS
),
};
const missingFields = [];
if (!provider.issuer) missingFields.push('OIDC_ISSUER_URL');
if (!provider.clientId) missingFields.push('OIDC_CLIENT_ID');
if (!provider.clientSecret) missingFields.push('OIDC_CLIENT_SECRET');
if (missingFields.length > 0) {
console.error(
`Cannot load OIDC provider "${provider.name}": missing required fields: ${missingFields.join(', ')}`
);
return [];
}
console.log(`Loaded OIDC provider: ${provider.name}`);
providers.push(provider);
}
if (providers.length === 0) {
console.warn(
'OIDC is enabled but no valid providers are configured. Please check your environment variables.'
);
}
return providers;
}
let cachedProviders = null;
function getAllProviders() {
if (!cachedProviders) {
cachedProviders = loadProvidersFromEnv();
}
return cachedProviders;
}
function getProvider(slug) {
const providers = getAllProviders();
const provider = providers.find((p) => p.slug === slug);
if (!provider) {
return null;
}
return provider;
}
function isOidcEnabled() {
return process.env.OIDC_ENABLED === 'true' && getAllProviders().length > 0;
}
function reloadProviders() {
cachedProviders = null;
return getAllProviders();
}
module.exports = {
getAllProviders,
getProvider,
isOidcEnabled,
reloadProviders,
};