feat(auth): Zwei-Faktor-Authentisierung — TOTP (Authenticator-App) (v1.8.0)
Freiwillige MFA, du kannst sie für dich aktivieren; Login verlangt danach den zweiten Faktor. - Schema/Migration: User.totpSecret/totpConfirmedAt/mfaBackupCodes + Tabelle webauthn_credentials (WebAuthn folgt), otplib v12 - lib/mfa.ts: TOTP-Secret/QR-URI/Verify + bcrypt-gehashte Backup-Codes - lib/auth.ts: buildSessionForUser, createMfaToken/verifyMfaToken (10-Min-Zwischentoken), userHasMfa - Login-Flow: nach Passwort -> bei aktiver MFA kein Cookie, sondern mfaToken; öffentlicher /api/auth/mfa-login/verify prüft TOTP ODER Backup-Code und setzt dann die Session (Rate-Limit) - Einrichtung in /settings: QR scannen, Code bestätigen, Backup-Codes (einmalig), deaktivieren - Admin-Reset-API: /api/admin/users/[id]/mfa-reset (SERVER_ADMIN alle, TENANT_ADMIN eigene Org) - Login-Seite: zweiter Schritt für Code/Backup-Code - SMS bewusst weggelassen (Kosten/SIM-Swap) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -185,6 +185,22 @@ async function migrate() {
|
||||
`CREATE INDEX IF NOT EXISTS legal_acceptances_doc_idx ON legal_acceptances("documentType", "documentVersion")`,
|
||||
`ALTER TABLE legal_acceptances ADD COLUMN IF NOT EXISTS "ipAddress" TEXT`,
|
||||
`ALTER TABLE legal_acceptances ADD COLUMN IF NOT EXISTS "userAgent" TEXT`,
|
||||
// MFA / Zwei-Faktor
|
||||
`ALTER TABLE users ADD COLUMN IF NOT EXISTS "totpSecret" TEXT`,
|
||||
`ALTER TABLE users ADD COLUMN IF NOT EXISTS "totpConfirmedAt" TIMESTAMP(3)`,
|
||||
`ALTER TABLE users ADD COLUMN IF NOT EXISTS "mfaBackupCodes" TEXT[] NOT NULL DEFAULT '{}'`,
|
||||
`CREATE TABLE IF NOT EXISTS webauthn_credentials (
|
||||
id TEXT PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
"userId" TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
"credentialId" TEXT NOT NULL UNIQUE,
|
||||
"publicKey" TEXT NOT NULL,
|
||||
counter INTEGER NOT NULL DEFAULT 0,
|
||||
transports TEXT,
|
||||
"deviceName" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"lastUsedAt" TIMESTAMP(3)
|
||||
)`,
|
||||
`CREATE INDEX IF NOT EXISTS webauthn_credentials_user_idx ON webauthn_credentials("userId")`,
|
||||
]
|
||||
for (const sql of tableMigrations) {
|
||||
try { await prisma.$executeRawUnsafe(sql) } catch (e) { /* table might already exist */ }
|
||||
|
||||
@@ -126,6 +126,12 @@ model User {
|
||||
resetToken String? @unique
|
||||
resetTokenExpiry DateTime?
|
||||
lastLoginAt DateTime?
|
||||
// ─── Zwei-Faktor-Authentisierung (MFA) ───
|
||||
// TOTP (Authenticator-App): Secret gesetzt = Einrichtung begonnen; confirmedAt gesetzt = aktiv.
|
||||
totpSecret String?
|
||||
totpConfirmedAt DateTime?
|
||||
// Einmalige Backup-Codes (bcrypt-gehasht), Fallback wenn kein Faktor verfügbar.
|
||||
mfaBackupCodes String[] @default([])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@ -135,10 +141,29 @@ model User {
|
||||
upgradeRequests UpgradeRequest[]
|
||||
rapports Rapport[]
|
||||
legalAcceptances LegalAcceptance[]
|
||||
webauthnCredentials WebAuthnCredential[]
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
// WebAuthn/FIDO2 (YubiKey, Passkeys, Touch-ID) — registrierte Sicherheitsschlüssel je Benutzer.
|
||||
model WebAuthnCredential {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
credentialId String @unique
|
||||
publicKey String
|
||||
counter Int @default(0)
|
||||
transports String?
|
||||
deviceName String?
|
||||
createdAt DateTime @default(now())
|
||||
lastUsedAt DateTime?
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
@@map("webauthn_credentials")
|
||||
}
|
||||
|
||||
// ─── Rechtsdokumente & revisionssichere Zustimmungen ──────────
|
||||
// Dokumenttypen (als String gehalten, damit die idempotente Raw-SQL-Migration einfach bleibt):
|
||||
// TERMS | PRIVACY | ORGANIZATION_DECLARATION | RESPONSIBLE_USE | DATA_PROCESSING_AGREEMENT
|
||||
|
||||
Reference in New Issue
Block a user