feat(security): let an operator require a second factor

Wire the previously-unused mfaEnforced flag into a real policy. An admin can
require a second factor instance-wide (an AppSettings toggle in the General
section) or per user (a toggle in user management); the effective requirement is
the OR of the two. When required and the account has no active factor — no
confirmed TOTP, no registered security key, and no primary passkey — the user is
walled behind a forced-enrollment interstitial after sign-in, with only the
security settings and sign-out reachable until a factor is enrolled.

The gate is surfaced like the onboarding gate: a hint cookie the proxy reads
without a DB round-trip, recomputed from the database on every sign-in and on
/api/auth/me so a tampered cookie self-corrects. Nobody is locked out — the
enrollment surface stays reachable, and toggling the policy off (or the
disable-mfa CLI) is the operator's recovery path.
This commit is contained in:
Marc-André Bombeck committed 2026-06-27 21:49:53 +02:00
1 parent 135bb04f0d
commit c8bef61344
22 files changed
+431 -8

No files matched your search

+12 -1
View File
@@ -278,6 +278,11 @@
"useAuthenticator": "Authenticator-App verwenden",
"backToLogin": "Zurück zur Anmeldung",
"rememberDevice": "Diesem Gerät 30 Tage vertrauen"
},
"enrollMfa": {
"title": "Zweiten Faktor einrichten",
"body": "Dein Administrator verlangt einen zweiten Faktor für dieses Konto. Füge eine Authenticator-App oder einen Sicherheitsschlüssel hinzu, um fortzufahren.",
"signOut": "Abmelden"
}
},
"dashboard": {
@@ -5627,7 +5632,13 @@
}
},
"carrier": "Anbieter",
"carrierUnavailableGeoFallback": "{location} — Anbieter nicht verfügbar"
"carrierUnavailableGeoFallback": "{location} — Anbieter nicht verfügbar",
"mfaRequired": "Zweiten Faktor verlangen",
"mfaRequiredDescription": "Jedes Konto ohne aktiven zweiten Faktor wird nach der Anmeldung zur Einrichtung geleitet.",
"mfaEnforcedOn": "MFA an",
"mfaEnforcedOff": "MFA aus",
"mfaEnforcedOnHint": "Für diesen Nutzer ist ein zweiter Faktor erforderlich. Klicken, um die Anforderung aufzuheben.",
"mfaEnforcedOffHint": "Einen zweiten Faktor für diesen Nutzer verlangen."
},
"achievements": {
"title": "Erfolge",
+12 -1
View File
@@ -278,6 +278,11 @@
"useAuthenticator": "Use your authenticator app",
"backToLogin": "Back to login",
"rememberDevice": "Trust this device for 30 days"
},
"enrollMfa": {
"title": "Set up a second factor",
"body": "Your administrator requires a second factor on this account. Add an authenticator app or a security key to continue.",
"signOut": "Sign out"
}
},
"dashboard": {
@@ -5627,7 +5632,13 @@
}
},
"carrier": "Carrier",
"carrierUnavailableGeoFallback": "{location} — carrier unavailable"
"carrierUnavailableGeoFallback": "{location} — carrier unavailable",
"mfaRequired": "Require a second factor",
"mfaRequiredDescription": "Every account without an active second factor is sent to enrollment after sign-in.",
"mfaEnforcedOn": "MFA on",
"mfaEnforcedOff": "MFA off",
"mfaEnforcedOnHint": "A second factor is required for this user. Click to stop requiring it.",
"mfaEnforcedOffHint": "Require a second factor for this user."
},
"achievements": {
"title": "Achievements",
+12 -1
View File
@@ -278,6 +278,11 @@
"useAuthenticator": "Usar tu aplicación de autenticación",
"backToLogin": "Volver al inicio de sesión",
"rememberDevice": "Confiar en este dispositivo durante 30 días"
},
"enrollMfa": {
"title": "Configurar un segundo factor",
"body": "Tu administrador exige un segundo factor en esta cuenta. Añade una app de autenticación o una llave de seguridad para continuar.",
"signOut": "Cerrar sesión"
}
},
"dashboard": {
@@ -5627,7 +5632,13 @@
}
},
"carrier": "Operador",
"carrierUnavailableGeoFallback": "{location} — operador no disponible"
"carrierUnavailableGeoFallback": "{location} — operador no disponible",
"mfaRequired": "Exigir un segundo factor",
"mfaRequiredDescription": "Toda cuenta sin un segundo factor activo se dirige a la configuración tras iniciar sesión.",
"mfaEnforcedOn": "MFA activado",
"mfaEnforcedOff": "MFA desactivado",
"mfaEnforcedOnHint": "Este usuario debe usar un segundo factor. Haz clic para dejar de exigirlo.",
"mfaEnforcedOffHint": "Exigir un segundo factor a este usuario."
},
"achievements": {
"title": "Logros",
+12 -1
View File
@@ -278,6 +278,11 @@
"useAuthenticator": "Utiliser votre application d'authentification",
"backToLogin": "Retour à la connexion",
"rememberDevice": "Faire confiance à cet appareil pendant 30 jours"
},
"enrollMfa": {
"title": "Configurer un second facteur",
"body": "Votre administrateur exige un second facteur sur ce compte. Ajoutez une application d'authentification ou une clé de sécurité pour continuer.",
"signOut": "Se déconnecter"
}
},
"dashboard": {
@@ -5627,7 +5632,13 @@
}
},
"carrier": "Opérateur",
"carrierUnavailableGeoFallback": "{location} — opérateur indisponible"
"carrierUnavailableGeoFallback": "{location} — opérateur indisponible",
"mfaRequired": "Exiger un second facteur",
"mfaRequiredDescription": "Tout compte sans second facteur actif est dirigé vers la configuration après la connexion.",
"mfaEnforcedOn": "MFA activé",
"mfaEnforcedOff": "MFA désactivé",
"mfaEnforcedOnHint": "Un second facteur est requis pour cet utilisateur. Cliquez pour ne plus l'exiger.",
"mfaEnforcedOffHint": "Exiger un second facteur pour cet utilisateur."
},
"achievements": {
"title": "Réussites",
+12 -1
View File
@@ -278,6 +278,11 @@
"useAuthenticator": "Usa la tua app di autenticazione",
"backToLogin": "Torna all'accesso",
"rememberDevice": "Considera attendibile questo dispositivo per 30 giorni"
},
"enrollMfa": {
"title": "Configura un secondo fattore",
"body": "Il tuo amministratore richiede un secondo fattore su questo account. Aggiungi un'app di autenticazione o una chiave di sicurezza per continuare.",
"signOut": "Esci"
}
},
"dashboard": {
@@ -5627,7 +5632,13 @@
}
},
"carrier": "Operatore",
"carrierUnavailableGeoFallback": "{location} — operatore non disponibile"
"carrierUnavailableGeoFallback": "{location} — operatore non disponibile",
"mfaRequired": "Richiedi un secondo fattore",
"mfaRequiredDescription": "Ogni account senza un secondo fattore attivo viene indirizzato alla configurazione dopo l'accesso.",
"mfaEnforcedOn": "MFA attivo",
"mfaEnforcedOff": "MFA disattivato",
"mfaEnforcedOnHint": "Per questo utente è richiesto un secondo fattore. Clicca per non richiederlo più.",
"mfaEnforcedOffHint": "Richiedi un secondo fattore per questo utente."
},
"achievements": {
"title": "Obiettivi",
+12 -1
View File
@@ -278,6 +278,11 @@
"useAuthenticator": "Użyj aplikacji uwierzytelniającej",
"backToLogin": "Powrót do logowania",
"rememberDevice": "Zaufaj temu urządzeniu na 30 dni"
},
"enrollMfa": {
"title": "Skonfiguruj drugi składnik",
"body": "Administrator wymaga drugiego składnika na tym koncie. Dodaj aplikację uwierzytelniającą lub klucz bezpieczeństwa, aby kontynuować.",
"signOut": "Wyloguj się"
}
},
"dashboard": {
@@ -5627,7 +5632,13 @@
}
},
"carrier": "Operator",
"carrierUnavailableGeoFallback": "{location} — operator niedostępny"
"carrierUnavailableGeoFallback": "{location} — operator niedostępny",
"mfaRequired": "Wymagaj drugiego składnika",
"mfaRequiredDescription": "Każde konto bez aktywnego drugiego składnika jest kierowane do konfiguracji po zalogowaniu.",
"mfaEnforcedOn": "MFA wł.",
"mfaEnforcedOff": "MFA wył.",
"mfaEnforcedOnHint": "Ten użytkownik musi używać drugiego składnika. Kliknij, aby przestać go wymagać.",
"mfaEnforcedOffHint": "Wymagaj drugiego składnika dla tego użytkownika."
},
"achievements": {
"title": "Osiągnięcia",