diff --git a/apps/api/src/app/auth/auth.module.ts b/apps/api/src/app/auth/auth.module.ts index 996cf397f..7e9e103ad 100644 --- a/apps/api/src/app/auth/auth.module.ts +++ b/apps/api/src/app/auth/auth.module.ts @@ -19,89 +19,6 @@ import { GoogleStrategy } from './google.strategy'; import { JwtStrategy } from './jwt.strategy'; import { OidcStrategy } from './oidc.strategy'; -// ANSI color codes -const colors = { - blue: '\x1b[34m', - reset: '\x1b[0m', - white: '\x1b[37m', - yellow: '\x1b[33m' -}; - -function validateOidcConfiguration( - configurationService: ConfigurationService -): void { - const missingVariables: string[] = []; - - // Common required variables for both configurations - const clientId = configurationService.get('OIDC_CLIENT_ID'); - const clientSecret = configurationService.get('OIDC_CLIENT_SECRET'); - const rootUrl = configurationService.get('ROOT_URL'); - - if (!clientId) { - missingVariables.push('OIDC_CLIENT_ID'); - } - - if (!clientSecret) { - missingVariables.push('OIDC_CLIENT_SECRET'); - } - - if (!rootUrl) { - missingVariables.push('ROOT_URL'); - } - - // Check for automatic or manual configuration - const authorizationUrl = configurationService.get('OIDC_AUTHORIZATION_URL'); - const issuer = configurationService.get('OIDC_ISSUER'); - const tokenUrl = configurationService.get('OIDC_TOKEN_URL'); - const userInfoUrl = configurationService.get('OIDC_USER_INFO_URL'); - - const hasAutomaticConfig = !!issuer; - const hasManualConfig = authorizationUrl || tokenUrl || userInfoUrl; - - if (!hasAutomaticConfig && !hasManualConfig) { - missingVariables.push( - 'OIDC_ISSUER (for automatic configuration) or OIDC_AUTHORIZATION_URL, OIDC_TOKEN_URL, OIDC_USER_INFO_URL (for manual configuration)' - ); - } else if (!hasAutomaticConfig && hasManualConfig) { - // Manual configuration: all three URLs are required - if (!authorizationUrl) { - missingVariables.push('OIDC_AUTHORIZATION_URL'); - } - - if (!tokenUrl) { - missingVariables.push('OIDC_TOKEN_URL'); - } - - if (!userInfoUrl) { - missingVariables.push('OIDC_USER_INFO_URL'); - } - } - - if (missingVariables.length > 0) { - const formattedVariables = missingVariables - .map( - (variable) => - ` ${colors.blue}${variable}:${colors.white} undefined${colors.reset}` - ) - .join('\n'); - - const errorMessage = ` -================================ - ${colors.yellow}Missing${colors.white} OIDC environment variables:${colors.reset} -${formattedVariables} - - ${colors.white}Configuration options:${colors.reset} - 1. Automatic: Set ${colors.blue}OIDC_ISSUER${colors.reset} (endpoints discovered automatically) - 2. Manual: Set ${colors.blue}OIDC_AUTHORIZATION_URL${colors.reset}, ${colors.blue}OIDC_TOKEN_URL${colors.reset}, ${colors.blue}OIDC_USER_INFO_URL${colors.reset} - - Both options require: ${colors.blue}ROOT_URL${colors.reset}, ${colors.blue}OIDC_CLIENT_ID${colors.reset}, ${colors.blue}OIDC_CLIENT_SECRET${colors.reset} -================================ -`; - Logger.error(errorMessage, 'OidcStrategy'); - process.exit(1); - } -} - @Module({ controllers: [AuthController], imports: [ @@ -137,8 +54,6 @@ ${formattedVariables} return null; } - validateOidcConfiguration(configurationService); - const issuer = configurationService.get('OIDC_ISSUER'); const scope = configurationService.get('OIDC_SCOPE'); @@ -146,9 +61,24 @@ ${formattedVariables} configurationService.get('OIDC_CALLBACK_URL') || `${configurationService.get('ROOT_URL')}/api/auth/oidc/callback`; - let options: StrategyOptions; - - if (issuer) { + // Check for manual URL overrides + const manualAuthorizationUrl = configurationService.get( + 'OIDC_AUTHORIZATION_URL' + ); + const manualTokenUrl = configurationService.get('OIDC_TOKEN_URL'); + const manualUserInfoUrl = + configurationService.get('OIDC_USER_INFO_URL'); + + let authorizationURL: string; + let tokenURL: string; + let userInfoURL: string; + + // If all manual URLs are provided, use them; otherwise fetch from discovery + if (manualAuthorizationUrl && manualTokenUrl && manualUserInfoUrl) { + authorizationURL = manualAuthorizationUrl; + tokenURL = manualTokenUrl; + userInfoURL = manualUserInfoUrl; + } else { try { const response = await fetch( `${issuer}/.well-known/openid-configuration` @@ -160,35 +90,28 @@ ${formattedVariables} userinfo_endpoint: string; }; - options = { - issuer, - scope, - authorizationURL: config.authorization_endpoint, - callbackURL: callbackUrl, - clientID: configurationService.get('OIDC_CLIENT_ID'), - clientSecret: configurationService.get('OIDC_CLIENT_SECRET'), - tokenURL: config.token_endpoint, - userInfoURL: config.userinfo_endpoint - }; + // Manual URLs take priority over discovered ones + authorizationURL = + manualAuthorizationUrl || config.authorization_endpoint; + tokenURL = manualTokenUrl || config.token_endpoint; + userInfoURL = manualUserInfoUrl || config.userinfo_endpoint; } catch (error) { Logger.error(error, 'OidcStrategy'); throw new Error('Failed to fetch OIDC configuration from issuer'); } - } else { - options = { - scope, - authorizationURL: configurationService.get( - 'OIDC_AUTHORIZATION_URL' - ), - callbackURL: callbackUrl, - clientID: configurationService.get('OIDC_CLIENT_ID'), - clientSecret: configurationService.get('OIDC_CLIENT_SECRET'), - issuer: configurationService.get('OIDC_ISSUER'), - tokenURL: configurationService.get('OIDC_TOKEN_URL'), - userInfoURL: configurationService.get('OIDC_USER_INFO_URL') - }; } + const options: StrategyOptions = { + issuer, + scope, + authorizationURL, + callbackURL: callbackUrl, + clientID: configurationService.get('OIDC_CLIENT_ID'), + clientSecret: configurationService.get('OIDC_CLIENT_SECRET'), + tokenURL, + userInfoURL + }; + return new OidcStrategy(authService, options); } }, diff --git a/apps/api/src/services/configuration/configuration.service.ts b/apps/api/src/services/configuration/configuration.service.ts index f31a8ca37..00029af8f 100644 --- a/apps/api/src/services/configuration/configuration.service.ts +++ b/apps/api/src/services/configuration/configuration.service.ts @@ -60,9 +60,18 @@ export class ConfigurationService { MAX_CHART_ITEMS: num({ default: 365 }), OIDC_AUTHORIZATION_URL: str({ default: '' }), OIDC_CALLBACK_URL: str({ default: '' }), - OIDC_CLIENT_ID: str({ default: '' }), - OIDC_CLIENT_SECRET: str({ default: '' }), - OIDC_ISSUER: str({ default: '' }), + OIDC_CLIENT_ID: str({ + default: undefined, + requiredWhen: (env) => env.ENABLE_FEATURE_AUTH_OIDC === true + }), + OIDC_CLIENT_SECRET: str({ + default: undefined, + requiredWhen: (env) => env.ENABLE_FEATURE_AUTH_OIDC === true + }), + OIDC_ISSUER: str({ + default: undefined, + requiredWhen: (env) => env.ENABLE_FEATURE_AUTH_OIDC === true + }), OIDC_SCOPE: json({ default: ['openid'] }), OIDC_TOKEN_URL: str({ default: '' }), OIDC_USER_INFO_URL: str({ default: '' }), diff --git a/apps/api/src/services/interfaces/environment.interface.ts b/apps/api/src/services/interfaces/environment.interface.ts index 3c03744f1..733e62b61 100644 --- a/apps/api/src/services/interfaces/environment.interface.ts +++ b/apps/api/src/services/interfaces/environment.interface.ts @@ -35,9 +35,9 @@ export interface Environment extends CleanedEnvAccessors { MAX_CHART_ITEMS: number; OIDC_AUTHORIZATION_URL: string; OIDC_CALLBACK_URL: string; - OIDC_CLIENT_ID: string; - OIDC_CLIENT_SECRET: string; - OIDC_ISSUER: string; + OIDC_CLIENT_ID: string | undefined; + OIDC_CLIENT_SECRET: string | undefined; + OIDC_ISSUER: string | undefined; OIDC_SCOPE: string[]; OIDC_TOKEN_URL: string; OIDC_USER_INFO_URL: string;