Feat: Seguridad avanzada para cambio de email y gestión de MFA

- Backend: Implementada lógica de tokens para cambio de mail y desactivación de 2FA.
- Frontend: Nuevos flujos de verificación en Perfil y Panel de Seguridad.
This commit is contained in:
2026-02-12 15:24:32 -03:00
parent 8c8c49894a
commit e096ed1590
10 changed files with 891 additions and 169 deletions

View File

@@ -15,16 +15,19 @@ public class IdentityService : IIdentityService
private readonly IPasswordService _passwordService;
private readonly IEmailService _emailService;
private readonly IConfiguration _config;
private readonly ITokenService _tokenService;
public IdentityService(
MotoresV2DbContext v2Context,
IPasswordService passwordService,
IEmailService emailService,
ITokenService tokenService,
IConfiguration config)
{
_v2Context = v2Context;
_passwordService = passwordService;
_emailService = emailService;
_tokenService = tokenService;
_config = config;
}
@@ -333,4 +336,187 @@ public class IdentityService : IIdentityService
await _v2Context.SaveChangesAsync();
return user;
}
// 1. INICIAR CAMBIO DE EMAIL (Requiere MFA activo y código válido)
public async Task<(bool Success, string Message)> InitiateEmailChangeAsync(int userId, string newEmail, string mfaCode)
{
var user = await _v2Context.Users.FindAsync(userId);
if (user == null) return (false, "Usuario no encontrado.");
// Validar formato básico de email
if (!new System.ComponentModel.DataAnnotations.EmailAddressAttribute().IsValid(newEmail))
return (false, "El formato del correo electrónico no es válido.");
// REGLA DE ORO: Solo si tiene MFA activo
if (!user.IsMFAEnabled || string.IsNullOrEmpty(user.MFASecret))
return (false, "Por seguridad, debes tener la Autenticación de Dos Factores (2FA) activada para cambiar tu email.");
// Validar código TOTP (Google Authenticator)
bool isCodeValid = _tokenService.ValidateTOTP(user.MFASecret, mfaCode);
if (!isCodeValid)
return (false, "El código de autenticación (2FA) es incorrecto.");
// Verificar que el mail no esté usado por otro
bool emailExists = await _v2Context.Users.AnyAsync(u => u.Email == newEmail);
if (emailExists)
return (false, "El nuevo correo electrónico ya está registrado en otra cuenta.");
// Generar Token
var token = Convert.ToHexString(RandomNumberGenerator.GetBytes(32));
user.NewEmailCandidate = newEmail;
user.EmailChangeToken = token;
user.EmailChangeTokenExpiresAt = DateTime.UtcNow.AddMinutes(30);
await _v2Context.SaveChangesAsync();
// Enviar Email al NUEVO correo
var frontendUrl = _config["AppSettings:FrontendUrl"]?.Split(',')[0] ?? "http://localhost:5173";
var link = $"{frontendUrl}/confirmar-cambio-email?token={token}";
var body = $@"
<div style='font-family: Arial, sans-serif; padding: 20px; border: 1px solid #eee; border-radius: 10px;'>
<h2 style='color: #0051ff;'>Confirma tu nuevo correo</h2>
<p>Hemos recibido una solicitud para transferir tu cuenta de Motores Argentinos a esta dirección.</p>
<p>Si fuiste tú, haz clic en el siguiente enlace para confirmar el cambio:</p>
<div style='margin: 25px 0;'>
<a href='{link}' style='background-color: #0051ff; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; font-weight: bold;'>CONFIRMAR EMAIL</a>
</div>
<p style='font-size: 12px; color: #666;'>El enlace expira en 30 minutos.</p>
</div>";
try
{
await _emailService.SendEmailAsync(newEmail, "Confirmar cambio de correo", body);
}
catch
{
return (false, "No se pudo enviar el correo de confirmación. Verifica que la dirección sea correcta.");
}
return (true, "Se ha enviado un enlace de confirmación a tu NUEVO correo electrónico. Revísalo para finalizar.");
}
// 2. CONFIRMAR CAMBIO (Click en el link)
public async Task<(bool Success, string Message)> ConfirmEmailChangeAsync(string token)
{
var user = await _v2Context.Users.FirstOrDefaultAsync(u => u.EmailChangeToken == token);
if (user == null) return (false, "El enlace es inválido o ya fue utilizado.");
if (user.EmailChangeTokenExpiresAt < DateTime.UtcNow) return (false, "El enlace ha expirado. Debes iniciar el trámite nuevamente.");
string oldEmail = user.Email;
// Aplicar cambio
user.Email = user.NewEmailCandidate!;
user.UserName = user.NewEmailCandidate!; // Opcional: Si usas el email como username
// Limpiar
user.NewEmailCandidate = null;
user.EmailChangeToken = null;
user.EmailChangeTokenExpiresAt = null;
// Auditoría
_v2Context.AuditLogs.Add(new AuditLog
{
Action = "SECURITY_EMAIL_CHANGED",
Entity = "User",
EntityID = user.UserID,
UserID = user.UserID,
Details = $"Email principal cambiado de {oldEmail} a {user.Email}"
});
await _v2Context.SaveChangesAsync();
return (true, "Tu correo electrónico ha sido actualizado correctamente. Por favor inicia sesión con tu nuevo email.");
}
// 3. INICIAR DESACTIVACIÓN MFA (Envía código al mail actual)
public async Task<(bool Success, string Message)> InitiateMFADisableAsync(int userId)
{
var user = await _v2Context.Users.FindAsync(userId);
if (user == null) return (false, "Usuario no encontrado.");
if (!user.IsMFAEnabled) return (false, "La autenticación de dos factores no está activa.");
// Generar Token Numérico simple (6 dígitos)
var token = new Random().Next(100000, 999999).ToString();
user.SecurityActionToken = token;
user.SecurityActionTokenExpiresAt = DateTime.UtcNow.AddMinutes(10);
await _v2Context.SaveChangesAsync();
var body = $@"
<div style='font-family: Arial, sans-serif; padding: 20px; border: 1px solid #eee; border-radius: 10px;'>
<h2 style='color: #ef4444;'>Alerta de Seguridad</h2>
<p>Has solicitado <strong>DESACTIVAR</strong> la seguridad de dos factores (2FA) en tu cuenta.</p>
<p>Tu código de seguridad para confirmar esta acción es:</p>
<h1 style='letter-spacing: 5px; background: #f3f4f6; padding: 10px; display: inline-block; border-radius: 8px;'>{token}</h1>
<p>Si no fuiste tú, cambia tu contraseña inmediatamente.</p>
</div>";
await _emailService.SendEmailAsync(user.Email, "Código de Seguridad: Desactivar MFA", body);
return (true, "Por seguridad, hemos enviado un código de confirmación a tu correo electrónico.");
}
// 4. CONFIRMAR DESACTIVACIÓN
public async Task<(bool Success, string Message)> ConfirmMFADisableAsync(int userId, string token)
{
var user = await _v2Context.Users.FindAsync(userId);
if (user == null) return (false, "Usuario no encontrado.");
// Validar token
if (user.SecurityActionToken != token) return (false, "El código ingresado es incorrecto.");
if (user.SecurityActionTokenExpiresAt < DateTime.UtcNow) return (false, "El código ha expirado. Solicita uno nuevo.");
// Desactivar
user.IsMFAEnabled = false;
user.MFASecret = null;
user.SecurityActionToken = null;
user.SecurityActionTokenExpiresAt = null;
_v2Context.AuditLogs.Add(new AuditLog
{
Action = "SECURITY_MFA_DISABLED",
Entity = "User",
EntityID = user.UserID,
UserID = user.UserID,
Details = "MFA Desactivado mediante validación por correo."
});
await _v2Context.SaveChangesAsync();
return (true, "La autenticación de dos factores ha sido desactivada.");
}
// 5. INICIAR RECONFIGURACIÓN MFA (Envía código al mail actual)
public async Task<(bool Success, string Message)> InitiateMFAReconfigureAsync(int userId)
{
var user = await _v2Context.Users.FindAsync(userId);
if (user == null) return (false, "Usuario no encontrado.");
if (!user.IsMFAEnabled) return (false, "La seguridad 2FA no está activa para reconfigurar.");
// Reutilizamos el mismo token de seguridad
var token = new Random().Next(100000, 999999).ToString();
user.SecurityActionToken = token;
user.SecurityActionTokenExpiresAt = DateTime.UtcNow.AddMinutes(10);
await _v2Context.SaveChangesAsync();
var body = $@"
<div style='font-family: Arial, sans-serif; padding: 20px;'>
<h2 style='color: #f59e0b;'>Alerta de Seguridad</h2>
<p>Has solicitado <strong>RECONFIGURAR</strong> la seguridad de dos factores (2FA) en tu cuenta.</p>
<p>Tu código de seguridad para autorizar esta acción es:</p>
<h1 style='letter-spacing: 5px;'>{token}</h1>
<p>Si no fuiste tú, cambia tu contraseña inmediatamente.</p>
</div>";
await _emailService.SendEmailAsync(user.Email, "Código de Seguridad: Reconfigurar 2FA", body);
return (true, "Hemos enviado un código a tu correo para autorizar la reconfiguración.");
}
}