feat: two-factor authentication (TOTP + backup codes)

Uses better-auth's built-in twoFactor plugin rather than hand-rolling
TOTP — adds the two_factors table and users.twoFactorEnabled, wires
the server/client plugins (allowPasswordless: true so OAuth-only
accounts aren't locked out of managing 2FA), and adds a custom rate
limit for the verify endpoints (5/min — far stricter than the default
100/10s, since a 6-digit code has a much smaller keyspace than a
password).

New Settings → Security section walks through enable (password
confirm -> QR + backup codes -> confirm a live code before it's
actually turned on, per the plugin's default flow) and disable. New
/verify-2fa page handles the post-password mid-login step, with a
backup-code fallback. Verified live end-to-end: enable, confirm,
forced re-auth on next sign-in, TOTP accepted, backup code accepted
(single-use), disable.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Arnaud
2026-07-13 23:08:15 +02:00
parent 4e5f45a7e5
commit acc93de708
17 changed files with 5500 additions and 13 deletions
+36 -1
View File
@@ -592,7 +592,17 @@
"resendingSending": "Sending…",
"resendFailed": "Failed to resend",
"resendSuccess": "Verification email sent — check your inbox",
"or": "or"
"or": "or",
"twoFactorTitle": "Two-factor authentication",
"twoFactorAppDescription": "Enter the 6-digit code from your authenticator app.",
"twoFactorBackupDescription": "Enter one of your backup codes.",
"twoFactorCodeLabel": "Code",
"twoFactorBackupCodeLabel": "Backup code",
"twoFactorVerify": "Verify",
"twoFactorVerifying": "Verifying…",
"twoFactorInvalidCode": "Invalid code",
"twoFactorUseBackupCode": "Use a backup code instead",
"twoFactorUseApp": "Use your authenticator app instead"
},
"batchCooking": {
"title": "Batch cooking",
@@ -1174,6 +1184,31 @@
"downloadData": "Download my data",
"downloadDataDescription": "Get a JSON export of all the personal data Epicure holds about you — your profile, recipes, activity, and settings.",
"downloadDataButton": "Download my data",
"twoFactorTitle": "Two-factor authentication",
"twoFactorDescription": "Require a code from an authenticator app in addition to your password when signing in.",
"twoFactorEnableButton": "Enable",
"twoFactorDisableButton": "Disable",
"twoFactorEnabled": "Two-factor authentication enabled",
"twoFactorDisabled": "Two-factor authentication disabled",
"twoFactorEnableFailed": "Failed to start setup",
"twoFactorDisableFailed": "Failed to disable",
"twoFactorConfirmPasswordTitle": "Confirm your password",
"twoFactorConfirmPasswordDescription": "Enter your password to set up two-factor authentication.",
"twoFactorContinue": "Continue",
"twoFactorCancel": "Cancel",
"twoFactorSetupTitle": "Set up two-factor authentication",
"twoFactorSetupDescription": "Scan this code with your authenticator app (like Google Authenticator or 1Password), then enter the 6-digit code it shows.",
"twoFactorQrAlt": "QR code for two-factor authentication setup",
"twoFactorBackupCodesLabel": "Backup codes",
"twoFactorBackupCodesHint": "Save these somewhere safe — each one can be used once to sign in if you lose access to your authenticator app.",
"twoFactorCopyCodes": "Copy codes",
"twoFactorCodesCopied": "Copied",
"twoFactorCopyFailed": "Failed to copy",
"twoFactorCodeLabel": "6-digit code",
"twoFactorVerifyAndEnable": "Verify & enable",
"twoFactorInvalidCode": "Invalid code",
"twoFactorDisableTitle": "Disable two-factor authentication",
"twoFactorDisableDescription": "Your account will only need your password to sign in.",
"webhookCreateFailed": "Failed to create webhook",
"webhookDeleteFailed": "Failed to delete webhook",
"webhookUpdateFailed": "Failed to update webhook",
+36 -1
View File
@@ -583,7 +583,17 @@
"checkInboxVerification": "Vérifiez votre boîte de réception pour l'e-mail de vérification.",
"resendVerification": "Renvoyer l'e-mail de vérification",
"resendingSending": "Envoi en cours…",
"or": "ou"
"or": "ou",
"twoFactorTitle": "Authentification à deux facteurs",
"twoFactorAppDescription": "Saisissez le code à 6 chiffres de votre application d'authentification.",
"twoFactorBackupDescription": "Saisissez un de vos codes de secours.",
"twoFactorCodeLabel": "Code",
"twoFactorBackupCodeLabel": "Code de secours",
"twoFactorVerify": "Vérifier",
"twoFactorVerifying": "Vérification…",
"twoFactorInvalidCode": "Code invalide",
"twoFactorUseBackupCode": "Utiliser un code de secours",
"twoFactorUseApp": "Utiliser votre application d'authentification"
},
"batchCooking": {
"title": "Batch cooking",
@@ -1162,6 +1172,31 @@
"downloadData": "Télécharger mes données",
"downloadDataDescription": "Obtenez un export JSON de toutes les données personnelles qu'Epicure détient sur vous — votre profil, vos recettes, votre activité et vos paramètres.",
"downloadDataButton": "Télécharger mes données",
"twoFactorTitle": "Authentification à deux facteurs",
"twoFactorDescription": "Exiger un code d'une application d'authentification en plus de votre mot de passe pour vous connecter.",
"twoFactorEnableButton": "Activer",
"twoFactorDisableButton": "Désactiver",
"twoFactorEnabled": "Authentification à deux facteurs activée",
"twoFactorDisabled": "Authentification à deux facteurs désactivée",
"twoFactorEnableFailed": "Échec du démarrage de la configuration",
"twoFactorDisableFailed": "Échec de la désactivation",
"twoFactorConfirmPasswordTitle": "Confirmez votre mot de passe",
"twoFactorConfirmPasswordDescription": "Saisissez votre mot de passe pour configurer l'authentification à deux facteurs.",
"twoFactorContinue": "Continuer",
"twoFactorCancel": "Annuler",
"twoFactorSetupTitle": "Configurer l'authentification à deux facteurs",
"twoFactorSetupDescription": "Scannez ce code avec votre application d'authentification (comme Google Authenticator ou 1Password), puis saisissez le code à 6 chiffres qu'elle affiche.",
"twoFactorQrAlt": "Code QR pour la configuration de l'authentification à deux facteurs",
"twoFactorBackupCodesLabel": "Codes de secours",
"twoFactorBackupCodesHint": "Conservez-les en lieu sûr — chacun ne peut être utilisé qu'une fois pour vous connecter si vous perdez l'accès à votre application d'authentification.",
"twoFactorCopyCodes": "Copier les codes",
"twoFactorCodesCopied": "Copié",
"twoFactorCopyFailed": "Échec de la copie",
"twoFactorCodeLabel": "Code à 6 chiffres",
"twoFactorVerifyAndEnable": "Vérifier et activer",
"twoFactorInvalidCode": "Code invalide",
"twoFactorDisableTitle": "Désactiver l'authentification à deux facteurs",
"twoFactorDisableDescription": "Votre compte n'aura besoin que de votre mot de passe pour vous connecter.",
"webhookCreateFailed": "Échec de la création du webhook",
"webhookDeleteFailed": "Échec de la suppression du webhook",
"webhookUpdateFailed": "Échec de la mise à jour du webhook",