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