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

@@ -215,7 +215,7 @@ public class AuthController : ControllerBase
// Permite a un usuario logueado iniciar el proceso de MFA voluntariamente
[Authorize]
[HttpPost("init-mfa")]
public async Task<IActionResult> InitMFA()
public async Task<IActionResult> InitMFA([FromBody] ConfirmSecurityActionRequest request)
{
var userIdStr = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userIdStr)) return Unauthorized();
@@ -223,13 +223,22 @@ public class AuthController : ControllerBase
var user = await _context.Users.FindAsync(int.Parse(userIdStr));
if (user == null) return NotFound();
// Generar secreto si no tiene
if (string.IsNullOrEmpty(user.MFASecret))
// Si el MFA ya está activo, exigimos el token de seguridad
if (user.IsMFAEnabled)
{
user.MFASecret = _tokenService.GenerateBase32Secret();
await _context.SaveChangesAsync();
if (user.SecurityActionToken != request.Token || user.SecurityActionTokenExpiresAt < DateTime.UtcNow)
{
return BadRequest(new { message = "El código de seguridad es inválido o ha expirado." });
}
// Limpiamos el token una vez usado
user.SecurityActionToken = null;
user.SecurityActionTokenExpiresAt = null;
}
// Generar secreto si no tiene (o si se está reconfigurando)
user.MFASecret = _tokenService.GenerateBase32Secret();
await _context.SaveChangesAsync();
// 📝 AUDITORÍA
_context.AuditLogs.Add(new AuditLog
{
@@ -243,11 +252,7 @@ public class AuthController : ControllerBase
var qrUri = _tokenService.GetQrCodeUri(user.Email, user.MFASecret);
return Ok(new
{
qrUri = qrUri,
secret = user.MFASecret
});
return Ok(new { qrUri, secret = user.MFASecret });
}
[HttpPost("verify-mfa")]
@@ -312,33 +317,58 @@ public class AuthController : ControllerBase
});
}
// CAMBIO DE EMAIL
[Authorize]
[HttpPost("disable-mfa")]
public async Task<IActionResult> DisableMFA()
[HttpPost("initiate-email-change")]
public async Task<IActionResult> InitiateEmailChange([FromBody] InitiateEmailChangeRequest request)
{
var userIdStr = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userIdStr)) return Unauthorized();
var userId = int.Parse(User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "0");
var (success, message) = await _identityService.InitiateEmailChangeAsync(userId, request.NewEmail, request.MfaCode);
var user = await _context.Users.FindAsync(int.Parse(userIdStr));
if (user == null) return NotFound();
if (!success) return BadRequest(new { message });
return Ok(new { message });
}
// Desactivamos MFA y limpiamos el secreto por seguridad
user.IsMFAEnabled = false;
user.MFASecret = null;
[HttpPost("confirm-email-change")]
public async Task<IActionResult> ConfirmEmailChange([FromBody] ConfirmEmailChangeRequest request)
{
var (success, message) = await _identityService.ConfirmEmailChangeAsync(request.Token);
if (!success) return BadRequest(new { message });
return Ok(new { message });
}
// 📝 AUDITORÍA
_context.AuditLogs.Add(new AuditLog
{
Action = "MFA_DISABLED",
Entity = "User",
EntityID = user.UserID,
UserID = user.UserID,
Details = "Autenticación de dos factores desactivada por el usuario."
});
// DESACTIVAR MFA
[Authorize]
[HttpPost("initiate-mfa-disable")]
public async Task<IActionResult> InitiateMFADisable()
{
var userId = int.Parse(User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "0");
var (success, message) = await _identityService.InitiateMFADisableAsync(userId);
await _context.SaveChangesAsync();
if (!success) return BadRequest(new { message });
return Ok(new { message });
}
return Ok(new { message = "MFA Desactivado correctamente." });
[Authorize]
[HttpPost("confirm-mfa-disable")]
public async Task<IActionResult> ConfirmMFADisable([FromBody] ConfirmSecurityActionRequest request)
{
var userId = int.Parse(User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "0");
var (success, message) = await _identityService.ConfirmMFADisableAsync(userId, request.Token);
if (!success) return BadRequest(new { message });
return Ok(new { message });
}
[Authorize]
[HttpPost("initiate-mfa-reconfigure")]
public async Task<IActionResult> InitiateMFAReconfigure()
{
var userId = int.Parse(User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "0");
var (success, message) = await _identityService.InitiateMFAReconfigureAsync(userId);
if (!success) return BadRequest(new { message });
return Ok(new { message });
}
[HttpPost("migrate-password")]