Files
SIG-CM/src/SIGCM.Infrastructure/Services/AuthService.cs

165 lines
6.1 KiB
C#
Raw Normal View History

2026-01-05 10:30:04 -03:00
using Google.Apis.Auth;
using OtpNet;
using SIGCM.Application.DTOs;
using SIGCM.Application.Interfaces;
2026-01-05 10:30:04 -03:00
using SIGCM.Domain.Entities;
using SIGCM.Domain.Interfaces;
2026-01-05 10:30:04 -03:00
using System.Text;
namespace SIGCM.Infrastructure.Services;
public class AuthService : IAuthService
{
private readonly IUserRepository _userRepo;
private readonly ITokenService _tokenService;
public AuthService(IUserRepository userRepo, ITokenService tokenService)
{
2026-01-05 10:30:04 -03:00
_userRepo = userRepo;
_tokenService = tokenService;
}
2026-01-05 10:30:04 -03:00
// Inicio de sesión estándar con usuario y contraseña
public async Task<AuthResult> LoginAsync(string username, string password)
{
var user = await _userRepo.GetByUsernameAsync(username);
2026-01-05 10:30:04 -03:00
if (user == null) return new AuthResult { Success = false, ErrorMessage = "Credenciales inválidas" };
// Verificación de bloqueo de cuenta
if (user.LockoutEnd.HasValue && user.LockoutEnd.Value > DateTime.UtcNow)
return new AuthResult { Success = false, ErrorMessage = "Cuenta bloqueada temporalmente", IsLockedOut = true };
// Verificación de cuenta activa
if (!user.IsActive)
return new AuthResult { Success = false, ErrorMessage = "Cuenta desactivada" };
// Verificación de contraseña
bool valid = BCrypt.Net.BCrypt.Verify(password, user.PasswordHash);
2026-01-05 10:30:04 -03:00
if (!valid)
{
user.FailedLoginAttempts++;
if (user.FailedLoginAttempts >= 5) user.LockoutEnd = DateTime.UtcNow.AddMinutes(15);
await _userRepo.UpdateAsync(user);
return new AuthResult { Success = false, ErrorMessage = "Credenciales inválidas" };
}
// Si MFA está activo, no devolver token aún, pedir verificación
if (user.IsMfaEnabled)
{
2026-01-06 10:34:06 -03:00
return new AuthResult { Success = true, RequiresMfa = true, UserId = user.Id };
2026-01-05 10:30:04 -03:00
}
// Éxito: Reiniciar intentos y generar token
user.FailedLoginAttempts = 0;
user.LockoutEnd = null;
user.LastLogin = DateTime.UtcNow;
await _userRepo.UpdateAsync(user);
return new AuthResult
{
Success = true,
Token = _tokenService.GenerateToken(user),
2026-01-06 10:34:06 -03:00
RequiresPasswordChange = user.MustChangePassword,
UserId = user.Id
2026-01-05 10:30:04 -03:00
};
}
// Registro de nuevos usuarios (Public Web)
public async Task<AuthResult> RegisterAsync(string username, string email, string password)
{
if (await _userRepo.GetByUsernameAsync(username) != null)
return new AuthResult { Success = false, ErrorMessage = "El usuario ya existe" };
if (await _userRepo.GetByEmailAsync(email) != null)
return new AuthResult { Success = false, ErrorMessage = "El email ya está registrado" };
var user = new User
{
Username = username,
Email = email,
PasswordHash = BCrypt.Net.BCrypt.HashPassword(password),
Role = "User", // Rol por defecto para la web pública
CreatedAt = DateTime.UtcNow,
MustChangePassword = false
};
await _userRepo.CreateAsync(user);
2026-01-06 10:34:06 -03:00
return new AuthResult { Success = true, Token = _tokenService.GenerateToken(user), UserId = user.Id };
2026-01-05 10:30:04 -03:00
}
// Login mediante Google OAuth
public async Task<AuthResult> GoogleLoginAsync(string idToken)
{
try
{
var payload = await GoogleJsonWebSignature.ValidateAsync(idToken);
var user = await _userRepo.GetByGoogleIdAsync(payload.Subject)
?? await _userRepo.GetByEmailAsync(payload.Email);
2026-01-05 10:30:04 -03:00
if (user == null)
{
// Auto-registro mediante Google
user = new User
{
Username = payload.Email.Split('@')[0],
Email = payload.Email,
GoogleId = payload.Subject,
PasswordHash = "OAUTH_LOGIN_" + Guid.NewGuid().ToString(), // Hash dummy
Role = "User",
CreatedAt = DateTime.UtcNow,
MustChangePassword = false
};
user.Id = await _userRepo.CreateAsync(user);
}
else if (string.IsNullOrEmpty(user.GoogleId))
{
// Vincular cuenta existente con Google
user.GoogleId = payload.Subject;
await _userRepo.UpdateAsync(user);
}
2026-01-06 10:34:06 -03:00
if (user.IsMfaEnabled) return new AuthResult { Success = true, RequiresMfa = true, UserId = user.Id };
2026-01-05 10:30:04 -03:00
2026-01-06 10:34:06 -03:00
return new AuthResult { Success = true, Token = _tokenService.GenerateToken(user), UserId = user.Id };
2026-01-05 10:30:04 -03:00
}
catch (InvalidJwtException)
{
return new AuthResult { Success = false, ErrorMessage = "Token de Google inválido" };
}
}
// Genera un secreto para configurar MFA con aplicaciones tipo Google Authenticator
public async Task<string> GenerateMfaSecretAsync(int userId)
{
var user = await _userRepo.GetByIdAsync(userId);
if (user == null) throw new Exception("Usuario no encontrado");
var secretBytes = KeyGeneration.GenerateRandomKey(20);
var secret = Base32Encoding.ToString(secretBytes);
user.MfaSecret = secret;
await _userRepo.UpdateAsync(user);
return secret;
}
// Verifica el código TOTP ingresado por el usuario
public async Task<bool> VerifyMfaCodeAsync(int userId, string code)
{
var user = await _userRepo.GetByIdAsync(userId);
if (user == null || string.IsNullOrEmpty(user.MfaSecret)) return false;
var totp = new Totp(Base32Encoding.ToBytes(user.MfaSecret));
return totp.VerifyTotp(code, out _, new VerificationWindow(1, 1));
}
// Activa o desactiva MFA para el usuario
public async Task EnableMfaAsync(int userId, bool enabled)
{
var user = await _userRepo.GetByIdAsync(userId);
if (user == null) return;
user.IsMfaEnabled = enabled;
await _userRepo.UpdateAsync(user);
}
}