feat(auth): WebAuthn (YubiKey/Passkeys) + Admin-2FA-Reset-Button (v1.8.1)
Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled

- WebAuthn/FIDO2 via @simplewebauthn v13: Registrierung (Session) + Login-Faktor (öffentlich),
  zustandslose Challenge-Token (JWT), Zähler/Klon-Schutz, Credentials in webauthn_credentials
  - Routen: /api/auth/mfa/webauthn/register/{options,verify}, DELETE /webauthn/[id],
    /api/auth/mfa-login/webauthn/{options,verify}
- Settings: Sicherheitsschlüssel/Passkey hinzufügen + Liste + entfernen
- Login: "Mit Sicherheitsschlüssel/Passkey anmelden" (neben TOTP)
- Admin: 2FA-Zurücksetzen-Button in der Benutzerliste (nutzt bestehende mfa-reset-API)
- Sicherheitsseite + TOM-Doku: 2FA als vorhanden dokumentiert

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Pepe Ziberi
2026-07-23 18:15:03 +02:00
parent f588b6d361
commit f58f1b31bf
14 changed files with 686 additions and 26 deletions

View File

@@ -109,6 +109,15 @@ export function UsersTab() {
} catch { toast({ title: 'Fehler', variant: 'destructive' }) }
}
const handleResetUserMfa = async (targetUser: UserRecord) => {
if (!confirm(`Zwei-Faktor (2FA) von "${targetUser.name}" zurücksetzen? Der Benutzer meldet sich danach nur mit Passwort an, bis er 2FA neu einrichtet.`)) return
try {
const res = await fetch(`/api/admin/users/${targetUser.id}/mfa-reset`, { method: 'POST' })
if (res.ok) { toast({ title: '2FA zurückgesetzt' }) }
else { const err = await res.json(); toast({ title: 'Fehler', description: err.error, variant: 'destructive' }) }
} catch { toast({ title: 'Fehler', variant: 'destructive' }) }
}
const handleToggleUserVerified = async (targetUser: UserRecord) => {
try {
const newVal = !targetUser.emailVerified
@@ -251,6 +260,9 @@ export function UsersTab() {
<Button variant="ghost" size="icon" className="h-8 w-8" title="Passwort zurücksetzen" onClick={() => handleResetUserPassword(u)}>
<KeyRound className="w-4 h-4" />
</Button>
<Button variant="ghost" size="icon" className="h-8 w-8" title="Zwei-Faktor (2FA) zurücksetzen" onClick={() => handleResetUserMfa(u)}>
<ShieldCheck className="w-4 h-4" />
</Button>
<Button variant="ghost" size="icon" className="h-8 w-8" title="Bearbeiten" onClick={() => openEditUser(u)}>
<Pencil className="w-4 h-4" />
</Button>