2FA: setup, activación, login y baja
Objetivo
Verificación en dos pasos por TOTP (Google Authenticator, etc.), con backup codes para cuando el usuario pierde el dispositivo.
Cómo empieza
Usuario logueado va a Settings → Seguridad → “Activar” (petid-client, two-factor-setup.tsx).
Pasos principales
POST /two-factor/setup(setup-two-factor.use-case.ts) genera un secret TOTP y lo guarda cifrado enuser.twoFactorSecret— pero 2FA sigue inactivo hasta confirmar un código. Devuelve{ secret, otpauthUrl }para pintar el QR.- Usuario escanea + introduce un código de 6 dígitos →
POST /two-factor/enable(enable-two-factor.use-case.ts): verifica el TOTP contra el secret descifrado, marcatwoFactorEnabledAt, y genera 10 backup codes (issue-backup-codes.use-case.ts) que solo se devuelven en esta respuesta — no se pueden volver a consultar. - Login con 2FA activo:
POST /auth/loginno emite tokens, devuelve{ mfaRequired: true, challengeToken }(JWT firmado, corta vida). El cliente pide el código enmfa-challenge.tsxy llamaPOST /auth/login/2fa. verify-two-factor.use-case.tsacepta TOTP válido O un backup code sin usar (consumeBackupCode) — cualquiera de los dos completa el login.
Reglas de negocio
- Desactivar 2FA (
disable-two-factor.use-case.ts) exige reintroducir la contraseña — una sesión secuestrada no puede bajar el 2FA por sí sola. - Al desactivar se borran todos los backup codes (
backupCodes.deleteForUser). - Regenerar códigos invalida los anteriores (mismo patrón “se muestran una sola vez”).
Interacciones
TwoFactorEnabledEvent/TwoFactorDisabledEventdisparan email (2fa-enabled/2fa-disabled) y push (account-security-updated) — vernotification-template-routing.md.
Dónde seguir mirando
petid-api/src/application/auth/{setup,enable,verify,disable}-two-factor.use-case.tspetid-client/src/app/(home)/two-factor-setup.tsx,mfa-challenge.tsx
Casos especiales
- El secret TOTP nunca viaja en claro a la DB — pasa por
SecretCipher(cifrado simétrico, no hash: hace falta poder descifrarlo para verificar el código). EnableTwoFactorUseCasefalla conTwoFactorNotInitializedErrorsi se llama sin haber pasado por/setupantes (protege contra saltarse el paso 1).