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:
@@ -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.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user