From d1fedc72af8c17eeda7a5edf157db1c92467ba35 Mon Sep 17 00:00:00 2001 From: Aaron Po Date: Sat, 28 Feb 2026 23:18:35 -0500 Subject: [PATCH] feat: implement consolidated TokenService with token generation, validation, and refresh - Implement ITokenService interface with unified token handling - Add TokenService class supporting AccessToken, RefreshToken, and ConfirmationToken generation - Add ValidateAccessTokenAsync, ValidateRefreshTokenAsync, ValidateConfirmationTokenAsync methods - Add RefreshTokenAsync for token rotation with new access and refresh tokens - Include ValidatedToken and RefreshTokenResult records for type safety - Add unit tests for token validation and refresh operations - Support environment-based token secrets: ACCESS_TOKEN_SECRET, REFRESH_TOKEN_SECRET, CONFIRMATION_TOKEN_SECRET --- .../TokenServiceRefresh.test.cs | 162 ++++++++++ .../TokenServiceValidation.test.cs | 282 ++++++++++++++++++ .../Service/Service.Auth/ITokenService.cs | 191 +++++++----- 3 files changed, 561 insertions(+), 74 deletions(-) create mode 100644 src/Core/Service/Service.Auth.Tests/TokenServiceRefresh.test.cs create mode 100644 src/Core/Service/Service.Auth.Tests/TokenServiceValidation.test.cs diff --git a/src/Core/Service/Service.Auth.Tests/TokenServiceRefresh.test.cs b/src/Core/Service/Service.Auth.Tests/TokenServiceRefresh.test.cs new file mode 100644 index 0000000..a2634de --- /dev/null +++ b/src/Core/Service/Service.Auth.Tests/TokenServiceRefresh.test.cs @@ -0,0 +1,162 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using Domain.Entities; +using Domain.Exceptions; +using FluentAssertions; +using Infrastructure.Jwt; +using Infrastructure.Repository.Auth; +using Moq; + +namespace Service.Auth.Tests; + +public class TokenServiceRefreshTest +{ + private readonly Mock _tokenInfraMock; + private readonly Mock _authRepositoryMock; + private readonly TokenService _tokenService; + + public TokenServiceRefreshTest() + { + _tokenInfraMock = new Mock(); + _authRepositoryMock = new Mock(); + + // Set environment variables for tokens + Environment.SetEnvironmentVariable("ACCESS_TOKEN_SECRET", "test-access-secret-that-is-very-long-1234567890"); + Environment.SetEnvironmentVariable("REFRESH_TOKEN_SECRET", "test-refresh-secret-that-is-very-long-1234567890"); + Environment.SetEnvironmentVariable("CONFIRMATION_TOKEN_SECRET", "test-confirmation-secret-that-is-very-long-1234567890"); + + _tokenService = new TokenService( + _tokenInfraMock.Object, + _authRepositoryMock.Object + ); + } + + [Fact] + public async Task RefreshTokenAsync_WithValidRefreshToken_ReturnsNewTokens() + { + // Arrange + var userId = Guid.NewGuid(); + const string username = "testuser"; + const string refreshToken = "valid-refresh-token"; + + var claims = new List + { + new(JwtRegisteredClaimNames.Sub, userId.ToString()), + new(JwtRegisteredClaimNames.UniqueName, username), + new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + }; + + var claimsIdentity = new ClaimsIdentity(claims); + var principal = new ClaimsPrincipal(claimsIdentity); + + var userAccount = new UserAccount + { + UserAccountId = userId, + Username = username, + FirstName = "Test", + LastName = "User", + Email = "test@example.com", + DateOfBirth = new DateTime(1990, 1, 1), + }; + + // Mock the validation of refresh token + _tokenInfraMock + .Setup(x => x.ValidateJwtAsync(refreshToken, It.IsAny())) + .ReturnsAsync(principal); + + // Mock the generation of new tokens + _tokenInfraMock + .Setup(x => x.GenerateJwt(userId, username, It.IsAny(), It.IsAny())) + .Returns((Guid _, string _, DateTime _, string _) => $"generated-token-{Guid.NewGuid()}"); + + _authRepositoryMock + .Setup(x => x.GetUserByIdAsync(userId)) + .ReturnsAsync(userAccount); + + // Act + var result = await _tokenService.RefreshTokenAsync(refreshToken); + + // Assert + result.Should().NotBeNull(); + result.UserAccount.UserAccountId.Should().Be(userId); + result.UserAccount.Username.Should().Be(username); + result.AccessToken.Should().NotBeEmpty(); + result.RefreshToken.Should().NotBeEmpty(); + + _authRepositoryMock.Verify( + x => x.GetUserByIdAsync(userId), + Times.Once + ); + + // Verify tokens were generated (called twice - once for access, once for refresh) + _tokenInfraMock.Verify( + x => x.GenerateJwt(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Exactly(2) + ); + } + + [Fact] + public async Task RefreshTokenAsync_WithInvalidRefreshToken_ThrowsUnauthorizedException() + { + // Arrange + const string invalidToken = "invalid-refresh-token"; + + _tokenInfraMock + .Setup(x => x.ValidateJwtAsync(invalidToken, It.IsAny())) + .ThrowsAsync(new UnauthorizedException("Invalid refresh token")); + + // Act & Assert + await FluentActions.Invoking(async () => + await _tokenService.RefreshTokenAsync(invalidToken) + ).Should().ThrowAsync(); + } + + [Fact] + public async Task RefreshTokenAsync_WithExpiredRefreshToken_ThrowsUnauthorizedException() + { + // Arrange + const string expiredToken = "expired-refresh-token"; + + _tokenInfraMock + .Setup(x => x.ValidateJwtAsync(expiredToken, It.IsAny())) + .ThrowsAsync(new UnauthorizedException("Refresh token has expired")); + + // Act & Assert + await FluentActions.Invoking(async () => + await _tokenService.RefreshTokenAsync(expiredToken) + ).Should().ThrowAsync(); + } + + [Fact] + public async Task RefreshTokenAsync_WithNonExistentUser_ThrowsUnauthorizedException() + { + // Arrange + var userId = Guid.NewGuid(); + const string username = "testuser"; + const string refreshToken = "valid-refresh-token"; + + var claims = new List + { + new(JwtRegisteredClaimNames.Sub, userId.ToString()), + new(JwtRegisteredClaimNames.UniqueName, username), + new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + }; + + var claimsIdentity = new ClaimsIdentity(claims); + var principal = new ClaimsPrincipal(claimsIdentity); + + _tokenInfraMock + .Setup(x => x.ValidateJwtAsync(refreshToken, It.IsAny())) + .ReturnsAsync(principal); + + _authRepositoryMock + .Setup(x => x.GetUserByIdAsync(userId)) + .ReturnsAsync((UserAccount?)null); + + // Act & Assert + await FluentActions.Invoking(async () => + await _tokenService.RefreshTokenAsync(refreshToken) + ).Should().ThrowAsync() + .WithMessage("*User account not found*"); + } +} diff --git a/src/Core/Service/Service.Auth.Tests/TokenServiceValidation.test.cs b/src/Core/Service/Service.Auth.Tests/TokenServiceValidation.test.cs new file mode 100644 index 0000000..1c729c8 --- /dev/null +++ b/src/Core/Service/Service.Auth.Tests/TokenServiceValidation.test.cs @@ -0,0 +1,282 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using Domain.Entities; +using Domain.Exceptions; +using FluentAssertions; +using Infrastructure.Jwt; +using Infrastructure.Repository.Auth; +using Moq; + +namespace Service.Auth.Tests; + +public class TokenServiceValidationTest +{ + private readonly Mock _tokenInfraMock; + private readonly Mock _authRepositoryMock; + private readonly TokenService _tokenService; + + public TokenServiceValidationTest() + { + _tokenInfraMock = new Mock(); + _authRepositoryMock = new Mock(); + + // Set environment variables for tokens + Environment.SetEnvironmentVariable("ACCESS_TOKEN_SECRET", "test-access-secret-that-is-very-long-1234567890"); + Environment.SetEnvironmentVariable("REFRESH_TOKEN_SECRET", "test-refresh-secret-that-is-very-long-1234567890"); + Environment.SetEnvironmentVariable("CONFIRMATION_TOKEN_SECRET", "test-confirmation-secret-that-is-very-long-1234567890"); + + _tokenService = new TokenService( + _tokenInfraMock.Object, + _authRepositoryMock.Object + ); + } + + [Fact] + public async Task ValidateAccessTokenAsync_WithValidToken_ReturnsValidatedToken() + { + // Arrange + var userId = Guid.NewGuid(); + const string username = "testuser"; + const string token = "valid-access-token"; + + var claims = new List + { + new(JwtRegisteredClaimNames.Sub, userId.ToString()), + new(JwtRegisteredClaimNames.UniqueName, username), + new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + }; + + var claimsIdentity = new ClaimsIdentity(claims); + var principal = new ClaimsPrincipal(claimsIdentity); + + _tokenInfraMock + .Setup(x => x.ValidateJwtAsync(token, It.IsAny())) + .ReturnsAsync(principal); + + // Act + var result = + await _tokenService.ValidateAccessTokenAsync(token); + + // Assert + result.Should().NotBeNull(); + result.UserId.Should().Be(userId); + result.Username.Should().Be(username); + result.Principal.Should().NotBeNull(); + result.Principal.FindFirst(JwtRegisteredClaimNames.Sub)?.Value.Should().Be(userId.ToString()); + } + + [Fact] + public async Task ValidateRefreshTokenAsync_WithValidToken_ReturnsValidatedToken() + { + // Arrange + var userId = Guid.NewGuid(); + const string username = "testuser"; + const string token = "valid-refresh-token"; + + var claims = new List + { + new(JwtRegisteredClaimNames.Sub, userId.ToString()), + new(JwtRegisteredClaimNames.UniqueName, username), + new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + }; + + var claimsIdentity = new ClaimsIdentity(claims); + var principal = new ClaimsPrincipal(claimsIdentity); + + _tokenInfraMock + .Setup(x => x.ValidateJwtAsync(token, It.IsAny())) + .ReturnsAsync(principal); + + // Act + var result = + await _tokenService.ValidateRefreshTokenAsync(token); + + // Assert + result.Should().NotBeNull(); + result.UserId.Should().Be(userId); + result.Username.Should().Be(username); + } + + [Fact] + public async Task ValidateConfirmationTokenAsync_WithValidToken_ReturnsValidatedToken() + { + // Arrange + var userId = Guid.NewGuid(); + const string username = "testuser"; + const string token = "valid-confirmation-token"; + + var claims = new List + { + new(JwtRegisteredClaimNames.Sub, userId.ToString()), + new(JwtRegisteredClaimNames.UniqueName, username), + new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + }; + + var claimsIdentity = new ClaimsIdentity(claims); + var principal = new ClaimsPrincipal(claimsIdentity); + + _tokenInfraMock + .Setup(x => x.ValidateJwtAsync(token, It.IsAny())) + .ReturnsAsync(principal); + + // Act + var result = + await _tokenService.ValidateConfirmationTokenAsync(token); + + // Assert + result.Should().NotBeNull(); + result.UserId.Should().Be(userId); + result.Username.Should().Be(username); + } + + [Fact] + public async Task ValidateAccessTokenAsync_WithInvalidToken_ThrowsUnauthorizedException() + { + // Arrange + const string token = "invalid-token"; + + _tokenInfraMock + .Setup(x => x.ValidateJwtAsync(token, It.IsAny())) + .ThrowsAsync(new UnauthorizedException("Invalid token")); + + // Act & Assert + await FluentActions.Invoking(async () => + await _tokenService.ValidateAccessTokenAsync(token) + ).Should().ThrowAsync(); + } + + [Fact] + public async Task ValidateAccessTokenAsync_WithExpiredToken_ThrowsUnauthorizedException() + { + // Arrange + const string token = "expired-token"; + + _tokenInfraMock + .Setup(x => x.ValidateJwtAsync(token, It.IsAny())) + .ThrowsAsync(new UnauthorizedException( + "Token has expired" + )); + + // Act & Assert + await FluentActions.Invoking(async () => + await _tokenService.ValidateAccessTokenAsync(token) + ).Should().ThrowAsync(); + } + + [Fact] + public async Task ValidateAccessTokenAsync_WithMissingUserIdClaim_ThrowsUnauthorizedException() + { + // Arrange + const string username = "testuser"; + const string token = "token-without-user-id"; + + // Claims without Sub (user ID) + var claims = new List + { + new(JwtRegisteredClaimNames.UniqueName, username), + new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + }; + + var claimsIdentity = new ClaimsIdentity(claims); + var principal = new ClaimsPrincipal(claimsIdentity); + + _tokenInfraMock + .Setup(x => x.ValidateJwtAsync(token, It.IsAny())) + .ReturnsAsync(principal); + + // Act & Assert + await FluentActions.Invoking(async () => + await _tokenService.ValidateAccessTokenAsync(token) + ).Should().ThrowAsync() + .WithMessage("*missing required claims*"); + } + + [Fact] + public async Task ValidateAccessTokenAsync_WithMissingUsernameClaim_ThrowsUnauthorizedException() + { + // Arrange + var userId = Guid.NewGuid(); + const string token = "token-without-username"; + + // Claims without UniqueName (username) + var claims = new List + { + new(JwtRegisteredClaimNames.Sub, userId.ToString()), + new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + }; + + var claimsIdentity = new ClaimsIdentity(claims); + var principal = new ClaimsPrincipal(claimsIdentity); + + _tokenInfraMock + .Setup(x => x.ValidateJwtAsync(token, It.IsAny())) + .ReturnsAsync(principal); + + // Act & Assert + await FluentActions.Invoking(async () => + await _tokenService.ValidateAccessTokenAsync(token) + ).Should().ThrowAsync() + .WithMessage("*missing required claims*"); + } + + [Fact] + public async Task ValidateAccessTokenAsync_WithMalformedUserId_ThrowsUnauthorizedException() + { + // Arrange + const string username = "testuser"; + const string token = "token-with-malformed-user-id"; + + // Claims with invalid GUID format + var claims = new List + { + new(JwtRegisteredClaimNames.Sub, "not-a-valid-guid"), + new(JwtRegisteredClaimNames.UniqueName, username), + new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + }; + + var claimsIdentity = new ClaimsIdentity(claims); + var principal = new ClaimsPrincipal(claimsIdentity); + + _tokenInfraMock + .Setup(x => x.ValidateJwtAsync(token, It.IsAny())) + .ReturnsAsync(principal); + + // Act & Assert + await FluentActions.Invoking(async () => + await _tokenService.ValidateAccessTokenAsync(token) + ).Should().ThrowAsync() + .WithMessage("*malformed user ID*"); + } + + [Fact] + public async Task ValidateRefreshTokenAsync_WithInvalidToken_ThrowsUnauthorizedException() + { + // Arrange + const string token = "invalid-refresh-token"; + + _tokenInfraMock + .Setup(x => x.ValidateJwtAsync(token, It.IsAny())) + .ThrowsAsync(new UnauthorizedException("Invalid token")); + + // Act & Assert + await FluentActions.Invoking(async () => + await _tokenService.ValidateRefreshTokenAsync(token) + ).Should().ThrowAsync(); + } + + [Fact] + public async Task ValidateConfirmationTokenAsync_WithInvalidToken_ThrowsUnauthorizedException() + { + // Arrange + const string token = "invalid-confirmation-token"; + + _tokenInfraMock + .Setup(x => x.ValidateJwtAsync(token, It.IsAny())) + .ThrowsAsync(new UnauthorizedException("Invalid token")); + + // Act & Assert + await FluentActions.Invoking(async () => + await _tokenService.ValidateConfirmationTokenAsync(token) + ).Should().ThrowAsync(); + } +} diff --git a/src/Core/Service/Service.Auth/ITokenService.cs b/src/Core/Service/Service.Auth/ITokenService.cs index 9590031..6252579 100644 --- a/src/Core/Service/Service.Auth/ITokenService.cs +++ b/src/Core/Service/Service.Auth/ITokenService.cs @@ -1,5 +1,9 @@ +using System.Security.Claims; +using System.IdentityModel.Tokens.Jwt; using Domain.Entities; +using Domain.Exceptions; using Infrastructure.Jwt; +using Infrastructure.Repository.Auth; namespace Service.Auth; @@ -10,15 +14,13 @@ public enum TokenType ConfirmationToken, } -public interface ITokenService -{ - public string GenerateAccessToken(UserAccount user); - public string GenerateRefreshToken(UserAccount user); - public string GenerateConfirmationToken(UserAccount user); +public record ValidatedToken(Guid UserId, string Username, ClaimsPrincipal Principal); - public string GenerateToken(UserAccount user) - where T : struct, Enum; -} +public record RefreshTokenResult( + UserAccount UserAccount, + string RefreshToken, + string AccessToken +); public static class TokenServiceExpirationHours { @@ -27,87 +29,128 @@ public static class TokenServiceExpirationHours public const double ConfirmationTokenHours = 0.5; // 30 minutes } -public class TokenService(ITokenInfrastructure tokenInfrastructure) - : ITokenService +public interface ITokenService { - private readonly string _accessTokenSecret = - Environment.GetEnvironmentVariable("ACCESS_TOKEN_SECRET") - ?? throw new InvalidOperationException( - "ACCESS_TOKEN_SECRET environment variable is not set" - ); + string GenerateAccessToken(UserAccount user); + string GenerateRefreshToken(UserAccount user); + string GenerateConfirmationToken(UserAccount user); + string GenerateToken(UserAccount user) where T : struct, Enum; + Task ValidateAccessTokenAsync(string token); + Task ValidateRefreshTokenAsync(string token); + Task ValidateConfirmationTokenAsync(string token); + Task RefreshTokenAsync(string refreshTokenString); +} - private readonly string _refreshTokenSecret = - Environment.GetEnvironmentVariable("REFRESH_TOKEN_SECRET") - ?? throw new InvalidOperationException( - "REFRESH_TOKEN_SECRET environment variable is not set" - ); +public class TokenService : ITokenService +{ + private readonly ITokenInfrastructure _tokenInfrastructure; + private readonly IAuthRepository _authRepository; - private readonly string _confirmationTokenSecret = - Environment.GetEnvironmentVariable("CONFIRMATION_TOKEN_SECRET") - ?? throw new InvalidOperationException( - "CONFIRMATION_TOKEN_SECRET environment variable is not set" - ); + private readonly string _accessTokenSecret; + private readonly string _refreshTokenSecret; + private readonly string _confirmationTokenSecret; - public string GenerateAccessToken(UserAccount userAccount) + public TokenService( + ITokenInfrastructure tokenInfrastructure, + IAuthRepository authRepository + ) { - var jwtExpiresAt = DateTime.UtcNow.AddHours( - TokenServiceExpirationHours.AccessTokenHours - ); - return tokenInfrastructure.GenerateJwt( - userAccount.UserAccountId, - userAccount.Username, - jwtExpiresAt, - _accessTokenSecret - ); + _tokenInfrastructure = tokenInfrastructure; + _authRepository = authRepository; + + _accessTokenSecret = Environment.GetEnvironmentVariable("ACCESS_TOKEN_SECRET") + ?? throw new InvalidOperationException("ACCESS_TOKEN_SECRET environment variable is not set"); + + _refreshTokenSecret = Environment.GetEnvironmentVariable("REFRESH_TOKEN_SECRET") + ?? throw new InvalidOperationException("REFRESH_TOKEN_SECRET environment variable is not set"); + + _confirmationTokenSecret = Environment.GetEnvironmentVariable("CONFIRMATION_TOKEN_SECRET") + ?? throw new InvalidOperationException("CONFIRMATION_TOKEN_SECRET environment variable is not set"); } - public string GenerateRefreshToken(UserAccount userAccount) + public string GenerateAccessToken(UserAccount user) { - var jwtExpiresAt = DateTime.UtcNow.AddHours( - TokenServiceExpirationHours.RefreshTokenHours - ); - return tokenInfrastructure.GenerateJwt( - userAccount.UserAccountId, - userAccount.Username, - jwtExpiresAt, - _refreshTokenSecret - ); + var expiresAt = DateTime.UtcNow.AddHours(TokenServiceExpirationHours.AccessTokenHours); + return _tokenInfrastructure.GenerateJwt(user.UserAccountId, user.Username, expiresAt, _accessTokenSecret); } - public string GenerateConfirmationToken(UserAccount userAccount) + public string GenerateRefreshToken(UserAccount user) { - var jwtExpiresAt = DateTime.UtcNow.AddHours( - TokenServiceExpirationHours.ConfirmationTokenHours - ); - return tokenInfrastructure.GenerateJwt( - userAccount.UserAccountId, - userAccount.Username, - jwtExpiresAt, - _confirmationTokenSecret - ); + var expiresAt = DateTime.UtcNow.AddHours(TokenServiceExpirationHours.RefreshTokenHours); + return _tokenInfrastructure.GenerateJwt(user.UserAccountId, user.Username, expiresAt, _refreshTokenSecret); } - public string GenerateToken(UserAccount userAccount) - where T : struct, Enum + public string GenerateConfirmationToken(UserAccount user) { - var tokenType = typeof(T); - if (tokenType == typeof(TokenType)) - { - var tokenTypeValue = (TokenType) - Enum.Parse(tokenType, typeof(T).Name); - return tokenTypeValue switch - { - TokenType.AccessToken => GenerateAccessToken(userAccount), - TokenType.RefreshToken => GenerateRefreshToken(userAccount), - TokenType.ConfirmationToken => GenerateConfirmationToken( - userAccount - ), - _ => throw new InvalidOperationException("Invalid token type"), - }; - } - else - { + var expiresAt = DateTime.UtcNow.AddHours(TokenServiceExpirationHours.ConfirmationTokenHours); + return _tokenInfrastructure.GenerateJwt(user.UserAccountId, user.Username, expiresAt, _confirmationTokenSecret); + } + + public string GenerateToken(UserAccount user) where T : struct, Enum + { + if (typeof(T) != typeof(TokenType)) throw new InvalidOperationException("Invalid token type"); + + var tokenTypeName = typeof(T).Name; + if (!Enum.TryParse(typeof(TokenType), tokenTypeName, out var parsed)) + throw new InvalidOperationException("Invalid token type"); + + var tokenType = (TokenType)parsed; + return tokenType switch + { + TokenType.AccessToken => GenerateAccessToken(user), + TokenType.RefreshToken => GenerateRefreshToken(user), + TokenType.ConfirmationToken => GenerateConfirmationToken(user), + _ => throw new InvalidOperationException("Invalid token type"), + }; + } + + public async Task ValidateAccessTokenAsync(string token) + => await ValidateTokenInternalAsync(token, _accessTokenSecret, "access"); + + public async Task ValidateRefreshTokenAsync(string token) + => await ValidateTokenInternalAsync(token, _refreshTokenSecret, "refresh"); + + public async Task ValidateConfirmationTokenAsync(string token) + => await ValidateTokenInternalAsync(token, _confirmationTokenSecret, "confirmation"); + + private async Task ValidateTokenInternalAsync(string token, string secret, string tokenType) + { + try + { + var principal = await _tokenInfrastructure.ValidateJwtAsync(token, secret); + + var userIdClaim = principal.FindFirst(JwtRegisteredClaimNames.Sub)?.Value; + var usernameClaim = principal.FindFirst(JwtRegisteredClaimNames.UniqueName)?.Value; + + if (string.IsNullOrEmpty(userIdClaim) || string.IsNullOrEmpty(usernameClaim)) + throw new UnauthorizedException($"Invalid {tokenType} token: missing required claims"); + + if (!Guid.TryParse(userIdClaim, out var userId)) + throw new UnauthorizedException($"Invalid {tokenType} token: malformed user ID"); + + return new ValidatedToken(userId, usernameClaim, principal); } + catch (UnauthorizedException) + { + throw; + } + catch (Exception e) + { + throw new UnauthorizedException($"Failed to validate {tokenType} token: {e.Message}"); + } + } + + public async Task RefreshTokenAsync(string refreshTokenString) + { + var validated = await ValidateRefreshTokenAsync(refreshTokenString); + var user = await _authRepository.GetUserByIdAsync(validated.UserId); + if (user == null) + throw new UnauthorizedException("User account not found"); + + var newAccess = GenerateAccessToken(user); + var newRefresh = GenerateRefreshToken(user); + + return new RefreshTokenResult(user, newRefresh, newAccess); } }