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 public interface IUsuarioRepository
{ {
Task<Usuario?> GetByUsernameAsync(string username); 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;
using SIGCM2.Application.Abstractions.Persistence; using SIGCM2.Application.Abstractions.Persistence;
using SIGCM2.Application.Abstractions.Security; using SIGCM2.Application.Abstractions.Security;
using SIGCM2.Domain.Entities;
using SIGCM2.Domain.Exceptions; using SIGCM2.Domain.Exceptions;
using SIGCM2.Domain.Security;
namespace SIGCM2.Application.Auth.Login; namespace SIGCM2.Application.Auth.Login;
@@ -11,15 +13,27 @@ public sealed class LoginCommandHandler : ICommandHandler<LoginCommand, LoginRes
private readonly IUsuarioRepository _repository; private readonly IUsuarioRepository _repository;
private readonly IPasswordHasher _hasher; private readonly IPasswordHasher _hasher;
private readonly IJwtService _jwtService; private readonly IJwtService _jwtService;
private readonly IRefreshTokenRepository _refreshRepository;
private readonly IRefreshTokenGenerator _refreshGenerator;
private readonly IClientContext _clientContext;
private readonly AuthOptions _authOptions;
public LoginCommandHandler( public LoginCommandHandler(
IUsuarioRepository repository, IUsuarioRepository repository,
IPasswordHasher hasher, IPasswordHasher hasher,
IJwtService jwtService) IJwtService jwtService,
IRefreshTokenRepository refreshRepository,
IRefreshTokenGenerator refreshGenerator,
IClientContext clientContext,
AuthOptions authOptions)
{ {
_repository = repository; _repository = repository;
_hasher = hasher; _hasher = hasher;
_jwtService = jwtService; _jwtService = jwtService;
_refreshRepository = refreshRepository;
_refreshGenerator = refreshGenerator;
_clientContext = clientContext;
_authOptions = authOptions;
} }
public async Task<LoginResponseDto> Handle(LoginCommand command) public async Task<LoginResponseDto> Handle(LoginCommand command)
@@ -34,15 +48,24 @@ public sealed class LoginCommandHandler : ICommandHandler<LoginCommand, LoginRes
throw new InvalidCredentialsException(); throw new InvalidCredentialsException();
var accessToken = _jwtService.GenerateAccessToken(usuario); 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) var permisos = JsonSerializer.Deserialize<string[]>(usuario.PermisosJson)
?? Array.Empty<string>(); ?? Array.Empty<string>();
return new LoginResponseDto( return new LoginResponseDto(
AccessToken: accessToken, AccessToken: accessToken,
RefreshToken: refreshToken, RefreshToken: rawRefresh, // raw to client — never stored
ExpiresIn: 3600, ExpiresIn: _authOptions.AccessTokenMinutes * 60,
Usuario: new UsuarioDto( Usuario: new UsuarioDto(
Id: usuario.Id, Id: usuario.Id,
Nombre: $"{usuario.Nombre} {usuario.Apellido}".Trim(), Nombre: $"{usuario.Nombre} {usuario.Apellido}".Trim(),