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
145 lines
4.4 KiB
JavaScript
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,
|
|
};
|