Documentation

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)

  1. User active la 2FA → User.twoFactorEnabled = true + twoFactorMethod = "email" | "totp".
  2. À chaque login, lib/auth.ts détecte le flag et marque le JWT avec pending2fa: true.
    • Méthode email → code envoyé via issueTwoFactorCode
    • Méthode totp → rien à envoyer, l'app génère le code
  3. proxy.ts redirige vers /two-factor tant que pending2fa est vrai.
  4. La page accepte 6 chiffres (TOTP / email) OU un backup code XXXX-XXXX.
  5. Soumission appelle useSession().update({ action: "verify-2fa", code }).
  6. 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, pending2fa saute.

Anti-bruteforce + replay

verifyTwoFactorAttempt applique automatiquement :

  • Lockout : 5 essais ratés → 15 min (twoFactorFailedAttempts + twoFactorLockedUntil).
  • Anti-replay TOTP : twoFactorLastCounter stocké 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.$transaction avec 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ésactive

Le 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>.

Allez plus loin

On this page