feat: add OIDC-only mode to disable password authentication

When OIDC is configured, admins can now enable 'Disable password
authentication' in Admin → Settings → SSO. This blocks all password-
based login and registration, forcing users through the SSO identity
provider instead.

Backend:
- routes/admin.ts: expose oidc_only flag on GET /admin/oidc and accept
  it on PUT /admin/oidc (persisted to app_settings)
- routes/auth.ts: add isOidcOnlyMode() helper; block POST /auth/login,
  POST /auth/register (for non-first-user), and PUT /auth/me/password
  with HTTP 403 when OIDC-only mode is active
- routes/auth.ts: expose oidc_only_mode boolean in GET /auth/app-config

Frontend:
- AdminPage: toggle in OIDC/SSO settings section (oidc_only saved with
  rest of OIDC config on same Save button)
- LoginPage: when oidc_only_mode is active, replace form with a
  single-button OIDC redirect; hide register toggle
- SettingsPage: hide password change section when oidc_only_mode is on
- i18n (en/de): admin.oidcOnlyMode, admin.oidcOnlyModeHint,
  login.oidcOnly
This commit is contained in:
Stephen Wheet
2026-03-28 19:33:18 +00:00
parent 3f26a68f64
commit 9f8d3f8d99
7 changed files with 101 additions and 6 deletions

View File

@@ -71,6 +71,13 @@ export default function SettingsPage(): React.ReactElement {
const [currentPassword, setCurrentPassword] = useState<string>('')
const [newPassword, setNewPassword] = useState<string>('')
const [confirmPassword, setConfirmPassword] = useState<string>('')
const [oidcOnlyMode, setOidcOnlyMode] = useState<boolean>(false)
useEffect(() => {
authApi.getAppConfig?.().then((config) => {
if (config?.oidc_only_mode) setOidcOnlyMode(true)
}).catch(() => {})
}, [])
useEffect(() => {
setMapTileUrl(settings.map_tile_url || '')
@@ -398,6 +405,7 @@ export default function SettingsPage(): React.ReactElement {
</div>
{/* Change Password */}
{!oidcOnlyMode && (
<div style={{ paddingTop: 8, marginTop: 8, borderTop: '1px solid var(--border-secondary)' }}>
<label className="block text-sm font-medium text-slate-700 mb-3">{t('settings.changePassword')}</label>
<div className="space-y-3">
@@ -446,6 +454,7 @@ export default function SettingsPage(): React.ReactElement {
</button>
</div>
</div>
)}
<div className="flex items-center gap-4">
<div style={{ position: 'relative', flexShrink: 0 }}>