export type SupportedSsoProviderId = 'authentik' | 'workos' | 'keycloak'; export type SsoProtocol = 'oidc' | 'saml'; export type SsoLoginMode = 'oidc' | 'saml' | null; type EnvMap = Record; export interface GenericOidcProviderConfig { providerId: SupportedSsoProviderId; clientId: string; clientSecret: string; discoveryUrl?: string; issuer?: string; authorizationUrl?: string; tokenUrl?: string; userInfoUrl?: string; scopes: string[]; pkce?: boolean; requireIssuerValidation?: boolean; } export interface SsoTeamSyncConfig { enabled: boolean; claim: string | null; } export interface SsoProviderDiscovery { id: SupportedSsoProviderId; name: string; protocols: SsoProtocol[]; configured: boolean; loginMode: SsoLoginMode; callbackPath: string | null; teamSync: SsoTeamSyncConfig; samlFallback: { configured: boolean; loginUrl: string | null; }; warnings: string[]; } const DEFAULT_SCOPES = ['openid', 'email', 'profile']; function readEnv(env: EnvMap, key: string): string | undefined { const value = env[key]?.trim(); return value ? value : undefined; } function toDiscoveryUrl(issuer: string): string { return `${issuer.replace(/\/$/, '')}/.well-known/openid-configuration`; } function getTeamSyncClaim(env: EnvMap, envKey: string, fallbackClaim?: string): SsoTeamSyncConfig { const claim = readEnv(env, envKey) ?? fallbackClaim ?? null; return { enabled: claim !== null, claim, }; } function buildAuthentikConfig(env: EnvMap): GenericOidcProviderConfig | null { const issuer = readEnv(env, 'AUTHENTIK_ISSUER'); const clientId = readEnv(env, 'AUTHENTIK_CLIENT_ID'); const clientSecret = readEnv(env, 'AUTHENTIK_CLIENT_SECRET'); if (!issuer || !clientId || !clientSecret) { return null; } const baseIssuer = issuer.replace(/\/$/, ''); return { providerId: 'authentik', issuer: baseIssuer, clientId, clientSecret, discoveryUrl: toDiscoveryUrl(baseIssuer), authorizationUrl: `${baseIssuer}/application/o/authorize/`, tokenUrl: `${baseIssuer}/application/o/token/`, userInfoUrl: `${baseIssuer}/application/o/userinfo/`, scopes: DEFAULT_SCOPES, }; } function buildWorkosConfig(env: EnvMap): GenericOidcProviderConfig | null { const issuer = readEnv(env, 'WORKOS_ISSUER'); const clientId = readEnv(env, 'WORKOS_CLIENT_ID'); const clientSecret = readEnv(env, 'WORKOS_CLIENT_SECRET'); if (!issuer || !clientId || !clientSecret) { return null; } const normalizedIssuer = issuer.replace(/\/$/, ''); return { providerId: 'workos', issuer: normalizedIssuer, clientId, clientSecret, discoveryUrl: toDiscoveryUrl(normalizedIssuer), scopes: DEFAULT_SCOPES, pkce: true, requireIssuerValidation: true, }; } function buildKeycloakConfig(env: EnvMap): GenericOidcProviderConfig | null { const issuer = readEnv(env, 'KEYCLOAK_ISSUER'); const clientId = readEnv(env, 'KEYCLOAK_CLIENT_ID'); const clientSecret = readEnv(env, 'KEYCLOAK_CLIENT_SECRET'); if (!issuer || !clientId || !clientSecret) { return null; } const normalizedIssuer = issuer.replace(/\/$/, ''); return { providerId: 'keycloak', issuer: normalizedIssuer, clientId, clientSecret, discoveryUrl: toDiscoveryUrl(normalizedIssuer), scopes: DEFAULT_SCOPES, pkce: true, requireIssuerValidation: true, }; } function collectWarnings(env: EnvMap, provider: SupportedSsoProviderId): string[] { const prefix = provider.toUpperCase(); const oidcFields = [ `${prefix}_CLIENT_ID`, `${prefix}_CLIENT_SECRET`, `${prefix}_ISSUER`, ] as const; const presentOidcFields = oidcFields.filter((field) => readEnv(env, field)); const warnings: string[] = []; if (presentOidcFields.length > 0 && presentOidcFields.length < oidcFields.length) { const missing = oidcFields.filter((field) => !readEnv(env, field)); warnings.push(`${provider} OIDC is partially configured. Missing: ${missing.join(', ')}`); } return warnings; } export function buildGenericOidcProviderConfigs( env: EnvMap = process.env, ): GenericOidcProviderConfig[] { return [buildAuthentikConfig(env), buildWorkosConfig(env), buildKeycloakConfig(env)].filter( (config): config is GenericOidcProviderConfig => config !== null, ); } export function listSsoStartupWarnings(env: EnvMap = process.env): string[] { return ['authentik', 'workos', 'keycloak'].flatMap((provider) => collectWarnings(env, provider as SupportedSsoProviderId), ); } export function buildSsoDiscovery(env: EnvMap = process.env): SsoProviderDiscovery[] { const oidcConfigs = new Map( buildGenericOidcProviderConfigs(env).map((config) => [config.providerId, config]), ); const keycloakSamlLoginUrl = readEnv(env, 'KEYCLOAK_SAML_LOGIN_URL') ?? null; return [ { id: 'authentik', name: 'Authentik', protocols: ['oidc'], configured: oidcConfigs.has('authentik'), loginMode: oidcConfigs.has('authentik') ? 'oidc' : null, callbackPath: oidcConfigs.has('authentik') ? '/api/auth/oauth2/callback/authentik' : null, teamSync: getTeamSyncClaim(env, 'AUTHENTIK_TEAM_SYNC_CLAIM', 'groups'), samlFallback: { configured: false, loginUrl: null, }, warnings: collectWarnings(env, 'authentik'), }, { id: 'workos', name: 'WorkOS', protocols: ['oidc'], configured: oidcConfigs.has('workos'), loginMode: oidcConfigs.has('workos') ? 'oidc' : null, callbackPath: oidcConfigs.has('workos') ? '/api/auth/oauth2/callback/workos' : null, teamSync: getTeamSyncClaim(env, 'WORKOS_TEAM_SYNC_CLAIM', 'organization_id'), samlFallback: { configured: false, loginUrl: null, }, warnings: collectWarnings(env, 'workos'), }, { id: 'keycloak', name: 'Keycloak', protocols: ['oidc', 'saml'], configured: oidcConfigs.has('keycloak') || keycloakSamlLoginUrl !== null, loginMode: oidcConfigs.has('keycloak') ? 'oidc' : keycloakSamlLoginUrl ? 'saml' : null, callbackPath: oidcConfigs.has('keycloak') ? '/api/auth/oauth2/callback/keycloak' : null, teamSync: getTeamSyncClaim(env, 'KEYCLOAK_TEAM_SYNC_CLAIM', 'groups'), samlFallback: { configured: keycloakSamlLoginUrl !== null, loginUrl: keycloakSamlLoginUrl, }, warnings: collectWarnings(env, 'keycloak'), }, ]; }