feat(app): update LoginCommandHandler to persist hashed refresh token on login

This commit is contained in:
2026-04-14 13:28:16 -03:00
parent b79efc778a
commit 8bbd2b6f2a
3 changed files with 40 additions and 4 deletions

View File

@@ -5,4 +5,5 @@ namespace SIGCM2.Application.Abstractions.Persistence;
public interface IUsuarioRepository
{
Task<Usuario?> GetByUsernameAsync(string username);
Task<Usuario?> GetByIdAsync(int id, CancellationToken ct = default);
}

View File

@@ -0,0 +1,12 @@
namespace SIGCM2.Application.Auth;
/// <summary>
/// Configuration values for authentication token generation.
/// Populated from the "Jwt" configuration section via IOptions in the Infrastructure layer.
/// Lives in Application to avoid circular dependency with Infrastructure.
/// </summary>
public sealed class AuthOptions
{
public int AccessTokenMinutes { get; set; } = 60;
public int RefreshTokenDays { get; set; } = 7;
}

View File

@@ -2,7 +2,9 @@ using System.Text.Json;
using SIGCM2.Application.Abstractions;
using SIGCM2.Application.Abstractions.Persistence;
using SIGCM2.Application.Abstractions.Security;
using SIGCM2.Domain.Entities;
using SIGCM2.Domain.Exceptions;
using SIGCM2.Domain.Security;
namespace SIGCM2.Application.Auth.Login;
@@ -11,15 +13,27 @@ public sealed class LoginCommandHandler : ICommandHandler<LoginCommand, LoginRes
private readonly IUsuarioRepository _repository;
private readonly IPasswordHasher _hasher;
private readonly IJwtService _jwtService;
private readonly IRefreshTokenRepository _refreshRepository;
private readonly IRefreshTokenGenerator _refreshGenerator;
private readonly IClientContext _clientContext;
private readonly AuthOptions _authOptions;
public LoginCommandHandler(
IUsuarioRepository repository,
IPasswordHasher hasher,
IJwtService jwtService)
IJwtService jwtService,
IRefreshTokenRepository refreshRepository,
IRefreshTokenGenerator refreshGenerator,
IClientContext clientContext,
AuthOptions authOptions)
{
_repository = repository;
_hasher = hasher;
_jwtService = jwtService;
_refreshRepository = refreshRepository;
_refreshGenerator = refreshGenerator;
_clientContext = clientContext;
_authOptions = authOptions;
}
public async Task<LoginResponseDto> Handle(LoginCommand command)
@@ -34,15 +48,24 @@ public sealed class LoginCommandHandler : ICommandHandler<LoginCommand, LoginRes
throw new InvalidCredentialsException();
var accessToken = _jwtService.GenerateAccessToken(usuario);
var refreshToken = Guid.NewGuid().ToString("N"); // opaque, not persisted in UDT-001
// Generate and persist refresh token — only the hash hits the DB
var rawRefresh = _refreshGenerator.Generate();
var hash = TokenHasher.Sha256Base64Url(rawRefresh);
var now = DateTime.UtcNow;
var ttl = TimeSpan.FromDays(_authOptions.RefreshTokenDays);
var entity = RefreshToken.IssueForNewFamily(
usuario.Id, hash, now, ttl,
_clientContext.Ip, _clientContext.UserAgent);
await _refreshRepository.AddAsync(entity);
var permisos = JsonSerializer.Deserialize<string[]>(usuario.PermisosJson)
?? Array.Empty<string>();
return new LoginResponseDto(
AccessToken: accessToken,
RefreshToken: refreshToken,
ExpiresIn: 3600,
RefreshToken: rawRefresh, // raw to client — never stored
ExpiresIn: _authOptions.AccessTokenMinutes * 60,
Usuario: new UsuarioDto(
Id: usuario.Id,
Nombre: $"{usuario.Nombre} {usuario.Apellido}".Trim(),