feat(auth): split OIDC_ONLY into granular auth toggles

Replaces the coarse oidc_only + allow_registration settings with four
independent toggles: password_login, password_registration, oidc_login,
oidc_registration. Each can be enabled/disabled individually in
Admin > Settings without affecting the others.

- Add resolveAuthToggles() in authService.ts as the central resolver;
  falls back to legacy oidc_only/allow_registration keys when new keys
  are absent (backward compat)
- OIDC_ONLY env var still works and overrides DB toggles for password_*,
  with a visual lock in the admin UI when active
- Server enforces lockout prevention: cannot disable all login methods
- oidc_login gate added to OIDC /login and /callback routes
- Remove oidc_only toggle from OIDC settings panel; replaced by the
  granular toggles in the Settings tab
- Add 6 new resolveAuthToggles() unit tests; fix AUTH-DB-033 error
  message assertion
- Update OIDC_ONLY descriptions in README, docker-compose, Helm values,
  Unraid template, and .env.example to clarify override semantics

Closes #492
This commit is contained in:
jubnl
2026-04-11 20:21:22 +02:00
parent 2b1889b9a9
commit bfd2553d1e
28 changed files with 439 additions and 76 deletions
+11
View File
@@ -525,6 +525,17 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
'admin.invite.deleteError': 'فشل حذف رابط الدعوة',
'admin.allowRegistration': 'السماح بالتسجيل',
'admin.allowRegistrationHint': 'يمكن للمستخدمين الجدد التسجيل بأنفسهم',
'admin.authMethods': 'Authentication Methods',
'admin.passwordLogin': 'Password Login',
'admin.passwordLoginHint': 'Allow users to sign in with email and password',
'admin.passwordRegistration': 'Password Registration',
'admin.passwordRegistrationHint': 'Allow new users to register with email and password',
'admin.oidcLogin': 'SSO Login',
'admin.oidcLoginHint': 'Allow users to sign in with SSO',
'admin.oidcRegistration': 'SSO Auto-Provisioning',
'admin.oidcRegistrationHint': 'Automatically create accounts for new SSO users',
'admin.envOverrideHint': 'Password login settings are controlled by the OIDC_ONLY environment variable and cannot be changed here.',
'admin.lockoutWarning': 'At least one login method must remain enabled',
'admin.requireMfa': 'فرض المصادقة الثنائية (2FA)',
'admin.requireMfaHint': 'يجب على المستخدمين الذين لا يملكون 2FA إكمال الإعداد في الإعدادات قبل استخدام التطبيق.',
'admin.apiKeys': 'مفاتيح API',
+11
View File
@@ -489,6 +489,17 @@ const br: Record<string, string | { name: string; category: string }[]> = {
'admin.tabs.settings': 'Configurações',
'admin.allowRegistration': 'Permitir cadastro',
'admin.allowRegistrationHint': 'Novos usuários podem se cadastrar sozinhos',
'admin.authMethods': 'Authentication Methods',
'admin.passwordLogin': 'Password Login',
'admin.passwordLoginHint': 'Allow users to sign in with email and password',
'admin.passwordRegistration': 'Password Registration',
'admin.passwordRegistrationHint': 'Allow new users to register with email and password',
'admin.oidcLogin': 'SSO Login',
'admin.oidcLoginHint': 'Allow users to sign in with SSO',
'admin.oidcRegistration': 'SSO Auto-Provisioning',
'admin.oidcRegistrationHint': 'Automatically create accounts for new SSO users',
'admin.envOverrideHint': 'Password login settings are controlled by the OIDC_ONLY environment variable and cannot be changed here.',
'admin.lockoutWarning': 'At least one login method must remain enabled',
'admin.requireMfa': 'Exigir autenticação em dois fatores (2FA)',
'admin.requireMfaHint': 'Usuários sem 2FA precisam concluir a configuração em Configurações antes de usar o app.',
'admin.apiKeys': 'Chaves de API',
+11
View File
@@ -489,6 +489,17 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
'admin.tabs.settings': 'Nastavení',
'admin.allowRegistration': 'Povolit registraci',
'admin.allowRegistrationHint': 'Noví uživatelé se mohou sami registrovat',
'admin.authMethods': 'Authentication Methods',
'admin.passwordLogin': 'Password Login',
'admin.passwordLoginHint': 'Allow users to sign in with email and password',
'admin.passwordRegistration': 'Password Registration',
'admin.passwordRegistrationHint': 'Allow new users to register with email and password',
'admin.oidcLogin': 'SSO Login',
'admin.oidcLoginHint': 'Allow users to sign in with SSO',
'admin.oidcRegistration': 'SSO Auto-Provisioning',
'admin.oidcRegistrationHint': 'Automatically create accounts for new SSO users',
'admin.envOverrideHint': 'Password login settings are controlled by the OIDC_ONLY environment variable and cannot be changed here.',
'admin.lockoutWarning': 'At least one login method must remain enabled',
'admin.requireMfa': 'Vyžadovat dvoufázové ověření (2FA)',
'admin.requireMfaHint': 'Uživatelé bez 2FA musí dokončit nastavení v Nastavení před použitím aplikace.',
'admin.apiKeys': 'API klíče',
+11
View File
@@ -493,6 +493,17 @@ const de: Record<string, string | { name: string; category: string }[]> = {
'admin.tabs.settings': 'Einstellungen',
'admin.allowRegistration': 'Registrierung erlauben',
'admin.allowRegistrationHint': 'Neue Benutzer können sich selbst registrieren',
'admin.authMethods': 'Authentication Methods',
'admin.passwordLogin': 'Password Login',
'admin.passwordLoginHint': 'Allow users to sign in with email and password',
'admin.passwordRegistration': 'Password Registration',
'admin.passwordRegistrationHint': 'Allow new users to register with email and password',
'admin.oidcLogin': 'SSO Login',
'admin.oidcLoginHint': 'Allow users to sign in with SSO',
'admin.oidcRegistration': 'SSO Auto-Provisioning',
'admin.oidcRegistrationHint': 'Automatically create accounts for new SSO users',
'admin.envOverrideHint': 'Password login settings are controlled by the OIDC_ONLY environment variable and cannot be changed here.',
'admin.lockoutWarning': 'At least one login method must remain enabled',
'admin.requireMfa': 'Zwei-Faktor-Authentifizierung (2FA) für alle verlangen',
'admin.requireMfaHint': 'Benutzer ohne 2FA müssen die Einrichtung unter Einstellungen abschließen, bevor sie die App nutzen können.',
'admin.apiKeys': 'API-Schlüssel',
+11
View File
@@ -518,6 +518,17 @@ const en: Record<string, string | { name: string; category: string }[]> = {
'admin.tabs.settings': 'Settings',
'admin.allowRegistration': 'Allow Registration',
'admin.allowRegistrationHint': 'New users can register themselves',
'admin.authMethods': 'Authentication Methods',
'admin.passwordLogin': 'Password Login',
'admin.passwordLoginHint': 'Allow users to sign in with email and password',
'admin.passwordRegistration': 'Password Registration',
'admin.passwordRegistrationHint': 'Allow new users to register with email and password',
'admin.oidcLogin': 'SSO Login',
'admin.oidcLoginHint': 'Allow users to sign in with SSO',
'admin.oidcRegistration': 'SSO Auto-Provisioning',
'admin.oidcRegistrationHint': 'Automatically create accounts for new SSO users',
'admin.envOverrideHint': 'Password login settings are controlled by the OIDC_ONLY environment variable and cannot be changed here.',
'admin.lockoutWarning': 'At least one login method must remain enabled',
'admin.requireMfa': 'Require two-factor authentication (2FA)',
'admin.requireMfaHint': 'Users without 2FA must complete setup in Settings before using the app.',
'admin.apiKeys': 'API Keys',
+11
View File
@@ -487,6 +487,17 @@ const es: Record<string, string> = {
'admin.tabs.settings': 'Ajustes',
'admin.allowRegistration': 'Permitir el registro',
'admin.allowRegistrationHint': 'Los nuevos usuarios pueden registrarse por sí mismos',
'admin.authMethods': 'Authentication Methods',
'admin.passwordLogin': 'Password Login',
'admin.passwordLoginHint': 'Allow users to sign in with email and password',
'admin.passwordRegistration': 'Password Registration',
'admin.passwordRegistrationHint': 'Allow new users to register with email and password',
'admin.oidcLogin': 'SSO Login',
'admin.oidcLoginHint': 'Allow users to sign in with SSO',
'admin.oidcRegistration': 'SSO Auto-Provisioning',
'admin.oidcRegistrationHint': 'Automatically create accounts for new SSO users',
'admin.envOverrideHint': 'Password login settings are controlled by the OIDC_ONLY environment variable and cannot be changed here.',
'admin.lockoutWarning': 'At least one login method must remain enabled',
'admin.requireMfa': 'Exigir autenticación en dos factores (2FA)',
'admin.requireMfaHint': 'Los usuarios sin 2FA deben completar la configuración en Ajustes antes de usar la aplicación.',
'admin.apiKeys': 'Claves API',
+11
View File
@@ -489,6 +489,17 @@ const fr: Record<string, string> = {
'admin.tabs.settings': 'Paramètres',
'admin.allowRegistration': 'Autoriser les inscriptions',
'admin.allowRegistrationHint': 'Les nouveaux utilisateurs peuvent s\'inscrire eux-mêmes',
'admin.authMethods': 'Authentication Methods',
'admin.passwordLogin': 'Password Login',
'admin.passwordLoginHint': 'Allow users to sign in with email and password',
'admin.passwordRegistration': 'Password Registration',
'admin.passwordRegistrationHint': 'Allow new users to register with email and password',
'admin.oidcLogin': 'SSO Login',
'admin.oidcLoginHint': 'Allow users to sign in with SSO',
'admin.oidcRegistration': 'SSO Auto-Provisioning',
'admin.oidcRegistrationHint': 'Automatically create accounts for new SSO users',
'admin.envOverrideHint': 'Password login settings are controlled by the OIDC_ONLY environment variable and cannot be changed here.',
'admin.lockoutWarning': 'At least one login method must remain enabled',
'admin.requireMfa': 'Exiger l\'authentification à deux facteurs (2FA)',
'admin.requireMfaHint': 'Les utilisateurs sans 2FA doivent terminer la configuration dans Paramètres avant d\'utiliser l\'application.',
'admin.apiKeys': 'Clés API',
+11
View File
@@ -489,6 +489,17 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
'admin.tabs.settings': 'Beállítások',
'admin.allowRegistration': 'Regisztráció engedélyezése',
'admin.allowRegistrationHint': 'Új felhasználók regisztrálhatják magukat',
'admin.authMethods': 'Authentication Methods',
'admin.passwordLogin': 'Password Login',
'admin.passwordLoginHint': 'Allow users to sign in with email and password',
'admin.passwordRegistration': 'Password Registration',
'admin.passwordRegistrationHint': 'Allow new users to register with email and password',
'admin.oidcLogin': 'SSO Login',
'admin.oidcLoginHint': 'Allow users to sign in with SSO',
'admin.oidcRegistration': 'SSO Auto-Provisioning',
'admin.oidcRegistrationHint': 'Automatically create accounts for new SSO users',
'admin.envOverrideHint': 'Password login settings are controlled by the OIDC_ONLY environment variable and cannot be changed here.',
'admin.lockoutWarning': 'At least one login method must remain enabled',
'admin.requireMfa': 'Kétlépcsős hitelesítés (2FA) kötelezővé tétele',
'admin.requireMfaHint': 'A 2FA nélküli felhasználóknak a Beállításokban kell befejezniük a beállítást az alkalmazás használata előtt.',
'admin.apiKeys': 'API kulcsok',
+11
View File
@@ -489,6 +489,17 @@ const it: Record<string, string | { name: string; category: string }[]> = {
'admin.tabs.settings': 'Impostazioni',
'admin.allowRegistration': 'Consenti Registrazione',
'admin.allowRegistrationHint': 'I nuovi utenti possono registrarsi autonomamente',
'admin.authMethods': 'Authentication Methods',
'admin.passwordLogin': 'Password Login',
'admin.passwordLoginHint': 'Allow users to sign in with email and password',
'admin.passwordRegistration': 'Password Registration',
'admin.passwordRegistrationHint': 'Allow new users to register with email and password',
'admin.oidcLogin': 'SSO Login',
'admin.oidcLoginHint': 'Allow users to sign in with SSO',
'admin.oidcRegistration': 'SSO Auto-Provisioning',
'admin.oidcRegistrationHint': 'Automatically create accounts for new SSO users',
'admin.envOverrideHint': 'Password login settings are controlled by the OIDC_ONLY environment variable and cannot be changed here.',
'admin.lockoutWarning': 'At least one login method must remain enabled',
'admin.requireMfa': 'Richiedi autenticazione a due fattori (2FA)',
'admin.requireMfaHint': 'Gli utenti senza 2FA devono completare la configurazione in Impostazioni prima di usare l\'app.',
'admin.apiKeys': 'Chiavi API',
+11
View File
@@ -490,6 +490,17 @@ const nl: Record<string, string> = {
'admin.tabs.settings': 'Instellingen',
'admin.allowRegistration': 'Registratie toestaan',
'admin.allowRegistrationHint': 'Nieuwe gebruikers kunnen zichzelf registreren',
'admin.authMethods': 'Authentication Methods',
'admin.passwordLogin': 'Password Login',
'admin.passwordLoginHint': 'Allow users to sign in with email and password',
'admin.passwordRegistration': 'Password Registration',
'admin.passwordRegistrationHint': 'Allow new users to register with email and password',
'admin.oidcLogin': 'SSO Login',
'admin.oidcLoginHint': 'Allow users to sign in with SSO',
'admin.oidcRegistration': 'SSO Auto-Provisioning',
'admin.oidcRegistrationHint': 'Automatically create accounts for new SSO users',
'admin.envOverrideHint': 'Password login settings are controlled by the OIDC_ONLY environment variable and cannot be changed here.',
'admin.lockoutWarning': 'At least one login method must remain enabled',
'admin.requireMfa': 'Tweestapsverificatie (2FA) verplicht stellen',
'admin.requireMfaHint': 'Gebruikers zonder 2FA moeten de installatie in Instellingen voltooien voordat ze de app kunnen gebruiken.',
'admin.apiKeys': 'API-sleutels',
+11
View File
@@ -461,6 +461,17 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
'admin.tabs.settings': 'Ustawienia',
'admin.allowRegistration': 'Zezwól na rejestrację',
'admin.allowRegistrationHint': 'Nowi użytkownicy mogą się rejestrować samodzielnie',
'admin.authMethods': 'Authentication Methods',
'admin.passwordLogin': 'Password Login',
'admin.passwordLoginHint': 'Allow users to sign in with email and password',
'admin.passwordRegistration': 'Password Registration',
'admin.passwordRegistrationHint': 'Allow new users to register with email and password',
'admin.oidcLogin': 'SSO Login',
'admin.oidcLoginHint': 'Allow users to sign in with SSO',
'admin.oidcRegistration': 'SSO Auto-Provisioning',
'admin.oidcRegistrationHint': 'Automatically create accounts for new SSO users',
'admin.envOverrideHint': 'Password login settings are controlled by the OIDC_ONLY environment variable and cannot be changed here.',
'admin.lockoutWarning': 'At least one login method must remain enabled',
'admin.requireMfa': 'Wymagaj uwierzytelniania dwuskładnikowego (2FA)',
'admin.requireMfaHint': 'Użytkownicy bez 2FA muszą ukończyć konfigurację w Ustawieniach zanim zaczną korzystać z aplikacji.',
'admin.apiKeys': 'Klucze API',
+11
View File
@@ -490,6 +490,17 @@ const ru: Record<string, string> = {
'admin.tabs.settings': 'Настройки',
'admin.allowRegistration': 'Разрешить регистрацию',
'admin.allowRegistrationHint': 'Новые пользователи могут регистрироваться самостоятельно',
'admin.authMethods': 'Authentication Methods',
'admin.passwordLogin': 'Password Login',
'admin.passwordLoginHint': 'Allow users to sign in with email and password',
'admin.passwordRegistration': 'Password Registration',
'admin.passwordRegistrationHint': 'Allow new users to register with email and password',
'admin.oidcLogin': 'SSO Login',
'admin.oidcLoginHint': 'Allow users to sign in with SSO',
'admin.oidcRegistration': 'SSO Auto-Provisioning',
'admin.oidcRegistrationHint': 'Automatically create accounts for new SSO users',
'admin.envOverrideHint': 'Password login settings are controlled by the OIDC_ONLY environment variable and cannot be changed here.',
'admin.lockoutWarning': 'At least one login method must remain enabled',
'admin.requireMfa': 'Требовать двухфакторную аутентификацию (2FA)',
'admin.requireMfaHint': 'Пользователи без 2FA должны завершить настройку в разделе «Настройки» перед использованием приложения.',
'admin.apiKeys': 'API-ключи',
+11
View File
@@ -490,6 +490,17 @@ const zh: Record<string, string> = {
'admin.tabs.settings': '设置',
'admin.allowRegistration': '允许注册',
'admin.allowRegistrationHint': '新用户可以自行注册',
'admin.authMethods': 'Authentication Methods',
'admin.passwordLogin': 'Password Login',
'admin.passwordLoginHint': 'Allow users to sign in with email and password',
'admin.passwordRegistration': 'Password Registration',
'admin.passwordRegistrationHint': 'Allow new users to register with email and password',
'admin.oidcLogin': 'SSO Login',
'admin.oidcLoginHint': 'Allow users to sign in with SSO',
'admin.oidcRegistration': 'SSO Auto-Provisioning',
'admin.oidcRegistrationHint': 'Automatically create accounts for new SSO users',
'admin.envOverrideHint': 'Password login settings are controlled by the OIDC_ONLY environment variable and cannot be changed here.',
'admin.lockoutWarning': 'At least one login method must remain enabled',
'admin.requireMfa': '要求双因素身份验证(2FA',
'admin.requireMfaHint': '未启用 2FA 的用户必须先完成设置中的配置才能使用应用。',
'admin.apiKeys': 'API 密钥',
+11
View File
@@ -515,6 +515,17 @@ const zhTw: Record<string, string> = {
'admin.tabs.settings': '設定',
'admin.allowRegistration': '允許註冊',
'admin.allowRegistrationHint': '新使用者可以自行註冊',
'admin.authMethods': 'Authentication Methods',
'admin.passwordLogin': 'Password Login',
'admin.passwordLoginHint': 'Allow users to sign in with email and password',
'admin.passwordRegistration': 'Password Registration',
'admin.passwordRegistrationHint': 'Allow new users to register with email and password',
'admin.oidcLogin': 'SSO Login',
'admin.oidcLoginHint': 'Allow users to sign in with SSO',
'admin.oidcRegistration': 'SSO Auto-Provisioning',
'admin.oidcRegistrationHint': 'Automatically create accounts for new SSO users',
'admin.envOverrideHint': 'Password login settings are controlled by the OIDC_ONLY environment variable and cannot be changed here.',
'admin.lockoutWarning': 'At least one login method must remain enabled',
'admin.requireMfa': '要求雙因素身份驗證(2FA',
'admin.requireMfaHint': '未啟用 2FA 的使用者必須先完成設定中的配置才能使用應用。',
'admin.apiKeys': 'API 金鑰',
+94 -37
View File
@@ -46,7 +46,6 @@ interface OidcConfig {
client_secret: string
client_secret_set: boolean
display_name: string
oidc_only: boolean
discovery_url: string
}
@@ -192,11 +191,16 @@ export default function AdminPage(): React.ReactElement {
useEffect(() => { adminApi.getBagTracking().then(d => setBagTrackingEnabled(d.enabled)).catch(() => {}) }, [])
// OIDC config
const [oidcConfig, setOidcConfig] = useState<OidcConfig>({ issuer: '', client_id: '', client_secret: '', client_secret_set: false, display_name: '', oidc_only: false, discovery_url: '' })
const [oidcConfig, setOidcConfig] = useState<OidcConfig>({ issuer: '', client_id: '', client_secret: '', client_secret_set: false, display_name: '', discovery_url: '' })
const [savingOidc, setSavingOidc] = useState<boolean>(false)
// Registration toggle
const [allowRegistration, setAllowRegistration] = useState<boolean>(true)
// Auth toggles
const [passwordLogin, setPasswordLogin] = useState<boolean>(true)
const [passwordRegistration, setPasswordRegistration] = useState<boolean>(true)
const [oidcLogin, setOidcLogin] = useState<boolean>(true)
const [oidcRegistration, setOidcRegistration] = useState<boolean>(true)
const [envOverrideOidcOnly, setEnvOverrideOidcOnly] = useState<boolean>(false)
const [oidcConfigured, setOidcConfigured] = useState<boolean>(false)
const [requireMfa, setRequireMfa] = useState<boolean>(false)
// Invite links
@@ -268,7 +272,12 @@ export default function AdminPage(): React.ReactElement {
const loadAppConfig = async () => {
try {
const config = await authApi.getAppConfig()
setAllowRegistration(config.allow_registration)
setPasswordLogin(config.password_login ?? true)
setPasswordRegistration(config.password_registration ?? config.allow_registration ?? true)
setOidcLogin(config.oidc_login ?? true)
setOidcRegistration(config.oidc_registration ?? config.allow_registration ?? true)
setEnvOverrideOidcOnly(config.env_override_oidc_only ?? false)
setOidcConfigured(config.oidc_configured ?? false)
if (config.require_mfa !== undefined) setRequireMfa(!!config.require_mfa)
if (config.allowed_file_types) setAllowedFileTypes(config.allowed_file_types)
} catch (err: unknown) {
@@ -286,12 +295,12 @@ export default function AdminPage(): React.ReactElement {
}
}
const handleToggleRegistration = async (value) => {
setAllowRegistration(value)
const handleToggleAuthSetting = async (key: string, value: boolean, setter: (v: boolean) => void) => {
setter(value)
try {
await authApi.updateAppSettings({ allow_registration: value })
await authApi.updateAppSettings({ [key]: value })
} catch (err: unknown) {
setAllowRegistration(!value)
setter(!value)
toast.error(getApiErrorMessage(err, t('common.error')))
}
}
@@ -792,28 +801,94 @@ export default function AdminPage(): React.ReactElement {
{activeTab === 'settings' && (
<div className="space-y-6">
{/* Registration Toggle */}
{/* Authentication Methods */}
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
<div className="px-6 py-4 border-b border-slate-100">
<h2 className="font-semibold text-slate-900">{t('admin.allowRegistration')}</h2>
<h2 className="font-semibold text-slate-900">{t('admin.authMethods')}</h2>
</div>
<div className="p-6">
<div className="p-6 space-y-5">
{envOverrideOidcOnly && (
<p className="text-xs text-amber-600 bg-amber-50 border border-amber-200 rounded-lg px-3 py-2">
{t('admin.envOverrideHint')}
</p>
)}
{/* Password Login */}
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-700">{t('admin.allowRegistration')}</p>
<p className="text-xs text-slate-400 mt-0.5">{t('admin.allowRegistrationHint')}</p>
<p className="text-sm font-medium text-slate-700">{t('admin.passwordLogin')}</p>
<p className="text-xs text-slate-400 mt-0.5">{t('admin.passwordLoginHint')}</p>
</div>
<button
onClick={() => handleToggleRegistration(!allowRegistration)}
className="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
style={{ background: allowRegistration ? 'var(--text-primary)' : 'var(--border-primary)' }}
disabled={envOverrideOidcOnly || (!passwordLogin && !oidcLogin)}
onClick={() => handleToggleAuthSetting('password_login', !passwordLogin, setPasswordLogin)}
title={!passwordLogin && !oidcLogin ? t('admin.lockoutWarning') : undefined}
className="relative inline-flex h-6 w-11 items-center rounded-full transition-colors disabled:opacity-50"
style={{ background: passwordLogin ? 'var(--text-primary)' : 'var(--border-primary)' }}
>
<span
className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200"
style={{ transform: allowRegistration ? 'translateX(20px)' : 'translateX(0)' }}
style={{ transform: passwordLogin ? 'translateX(20px)' : 'translateX(0)' }}
/>
</button>
</div>
{/* Password Registration */}
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-700">{t('admin.passwordRegistration')}</p>
<p className="text-xs text-slate-400 mt-0.5">{t('admin.passwordRegistrationHint')}</p>
</div>
<button
disabled={envOverrideOidcOnly}
onClick={() => handleToggleAuthSetting('password_registration', !passwordRegistration, setPasswordRegistration)}
className="relative inline-flex h-6 w-11 items-center rounded-full transition-colors disabled:opacity-50"
style={{ background: passwordRegistration ? 'var(--text-primary)' : 'var(--border-primary)' }}
>
<span
className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200"
style={{ transform: passwordRegistration ? 'translateX(20px)' : 'translateX(0)' }}
/>
</button>
</div>
{/* SSO Login (only when OIDC configured) */}
{oidcConfigured && (
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-700">{t('admin.oidcLogin')}</p>
<p className="text-xs text-slate-400 mt-0.5">{t('admin.oidcLoginHint')}</p>
</div>
<button
disabled={!passwordLogin && oidcLogin}
onClick={() => handleToggleAuthSetting('oidc_login', !oidcLogin, setOidcLogin)}
title={!passwordLogin && oidcLogin ? t('admin.lockoutWarning') : undefined}
className="relative inline-flex h-6 w-11 items-center rounded-full transition-colors disabled:opacity-50"
style={{ background: oidcLogin ? 'var(--text-primary)' : 'var(--border-primary)' }}
>
<span
className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200"
style={{ transform: oidcLogin ? 'translateX(20px)' : 'translateX(0)' }}
/>
</button>
</div>
)}
{/* SSO Registration (only when OIDC configured) */}
{oidcConfigured && (
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-700">{t('admin.oidcRegistration')}</p>
<p className="text-xs text-slate-400 mt-0.5">{t('admin.oidcRegistrationHint')}</p>
</div>
<button
onClick={() => handleToggleAuthSetting('oidc_registration', !oidcRegistration, setOidcRegistration)}
className="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
style={{ background: oidcRegistration ? 'var(--text-primary)' : 'var(--border-primary)' }}
>
<span
className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200"
style={{ transform: oidcRegistration ? 'translateX(20px)' : 'translateX(0)' }}
/>
</button>
</div>
)}
</div>
</div>
@@ -1036,29 +1111,11 @@ export default function AdminPage(): React.ReactElement {
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent"
/>
</div>
{/* OIDC-only mode toggle */}
<div className="flex items-center justify-between pt-2 border-t border-slate-100">
<div>
<p className="text-sm font-medium text-slate-700">{t('admin.oidcOnlyMode')}</p>
<p className="text-xs text-slate-400 mt-0.5">{t('admin.oidcOnlyModeHint')}</p>
</div>
<button
onClick={() => setOidcConfig(c => ({ ...c, oidc_only: !c.oidc_only }))}
className="relative inline-flex h-6 w-11 items-center rounded-full transition-colors flex-shrink-0 ml-4"
style={{ background: oidcConfig.oidc_only ? 'var(--text-primary)' : 'var(--border-primary)' }}
>
<span
className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200"
style={{ transform: oidcConfig.oidc_only ? 'translateX(20px)' : 'translateX(0)' }}
/>
</button>
</div>
<button
onClick={async () => {
setSavingOidc(true)
try {
const payload: Record<string, unknown> = { issuer: oidcConfig.issuer, client_id: oidcConfig.client_id, display_name: oidcConfig.display_name, oidc_only: oidcConfig.oidc_only, discovery_url: oidcConfig.discovery_url }
const payload: Record<string, unknown> = { issuer: oidcConfig.issuer, client_id: oidcConfig.client_id, display_name: oidcConfig.display_name, discovery_url: oidcConfig.discovery_url }
if (oidcConfig.client_secret) payload.client_secret = oidcConfig.client_secret
await adminApi.updateOidc(payload)
toast.success(t('admin.oidcSaved'))
+10 -5
View File
@@ -15,6 +15,11 @@ interface AppConfig {
oidc_configured: boolean
oidc_display_name?: string
oidc_only_mode: boolean
password_login: boolean
password_registration: boolean
oidc_login: boolean
oidc_registration: boolean
env_override_oidc_only: boolean
}
export default function LoginPage(): React.ReactElement {
@@ -104,7 +109,7 @@ export default function LoginPage(): React.ReactElement {
if (config) {
setAppConfig(config)
if (!config.has_users) setMode('register')
if (config.oidc_only_mode && config.oidc_configured && config.has_users && !invite && !noRedirect) {
if (!config.password_login && config.oidc_login && config.oidc_configured && config.has_users && !invite && !noRedirect) {
window.location.href = '/api/auth/oidc/login'
}
}
@@ -194,10 +199,10 @@ export default function LoginPage(): React.ReactElement {
}
}
const showRegisterOption = (appConfig?.allow_registration || !appConfig?.has_users || inviteValid) && !appConfig?.oidc_only_mode && (appConfig?.setup_complete !== false || !appConfig?.has_users)
const showRegisterOption = (appConfig?.password_registration || !appConfig?.has_users || inviteValid) && (appConfig?.setup_complete !== false || !appConfig?.has_users)
// In OIDC-only mode, show a minimal page that redirects directly to the IdP
const oidcOnly = appConfig?.oidc_only_mode && appConfig?.oidc_configured
const oidcOnly = !appConfig?.password_login && appConfig?.oidc_login && appConfig?.oidc_configured
const inputBase: React.CSSProperties = {
width: '100%', padding: '11px 12px 11px 40px', border: '1px solid #e5e7eb',
@@ -730,8 +735,8 @@ export default function LoginPage(): React.ReactElement {
</>)}
</div>
{/* OIDC / SSO login button (only when OIDC is configured but not in oidc-only mode) */}
{appConfig?.oidc_configured && !oidcOnly && (
{/* OIDC / SSO login button (only when OIDC is configured, oidc_login enabled, not in oidc-only mode) */}
{appConfig?.oidc_configured && appConfig?.oidc_login && !oidcOnly && (
<>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginTop: 16 }}>
<div style={{ flex: 1, height: 1, background: '#e5e7eb' }} />