2FA (email + TOTP)
Authentification à deux facteurs — code email ou Google Authenticator (TOTP), backup codes, anti-bruteforce.
Le boilerplate ship avec deux méthodes de 2FA :
- Email (par défaut) — code 6 chiffres envoyé par mail
- TOTP (Google Authenticator, 1Password, Authy…) — QR code + 10 backup codes
Le user choisit dans /account/settings → Sécurité.
Flow login (commun aux deux méthodes)
- User active la 2FA →
User.twoFactorEnabled = true+twoFactorMethod = "email" | "totp". - À chaque login,
lib/auth.tsdétecte le flag et marque le JWT avecpending2fa: true.- Méthode email → code envoyé via
issueTwoFactorCode - Méthode totp → rien à envoyer, l'app génère le code
- Méthode email → code envoyé via
proxy.tsredirige vers/two-factortant quepending2faest vrai.- La page accepte 6 chiffres (TOTP / email) OU un backup code
XXXX-XXXX. - Soumission appelle
useSession().update({ action: "verify-2fa", code }). - Le callback JWT délègue à
verifyTwoFactorAttempt(lib/two-factor-verify.ts) qui essaie TOTP → backup → email selon la méthode active. Sur succès,pending2fasaute.
Anti-bruteforce + replay
verifyTwoFactorAttempt applique automatiquement :
- Lockout : 5 essais ratés → 15 min (
twoFactorFailedAttempts+twoFactorLockedUntil). - Anti-replay TOTP :
twoFactorLastCounterstocké après chaque succès, on refuse tout code dont counter ≤ stocké → un code sniffé pendant sa fenêtre de 30s ne peut pas être rejoué. - Backup codes atomiques : consommation dans
prisma.$transactionavec re-read interne, pas de double-spend sur logins concurrents.
TOTP — enrollment
Routes :
POST /api/user/2fa/totp → { qrDataUrl, secret } (rien persisté)
PATCH /api/user/2fa/totp → { secret, code } → { backupCodes: [...] }
DELETE /api/user/2fa/totp → { code } (TOTP ou backup) → désactiveLe secret est généré via otplib (lib/totp.ts). On retourne un QR data:image/png;base64,… + le secret en base32 pour saisie manuelle. Tant que PATCH n'a pas validé, rien n'est écrit en DB → pas d'utilisateur "à demi-enrôlé".
10 backup codes (format ABCD-EFGH) sont générés à l'activation. Bcrypt-hashés (cost 8) en DB, affichés une seule fois côté client.
Désactiver TOTP sans authenticator
Le DELETE accepte un backup code en plus du code TOTP. Si l'user perd son téléphone, il utilise un backup code pour disable la 2FA et restaure son accès — cas d'usage exact des backup codes.
Email — durée de validité
Code 10 min. Constante dans lib/two-factor.ts : CODE_TTL_MS.
Modèle de données
model User {
twoFactorEnabled Boolean @default(false)
twoFactorMethod String @default("email")
twoFactorSecret String?
twoFactorLastCounter Int?
twoFactorBackupCodes Json?
twoFactorFailedAttempts Int @default(0)
twoFactorLockedUntil DateTime?
}L'email réutilise VerificationToken (Auth.js) avec identifier 2fa:<email>.