mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-10-15 01:54:43 +00:00
Migrate TokenService.RefreshTokenAsync to UserManager
Last remaining IAuthRepository call site; RefreshTokenResult now carries a plain (Guid, string) instead of UserAccount.
This commit is contained in:
@@ -1,4 +1,3 @@
|
|||||||
using Domain.Entities;
|
|
||||||
using Features.Auth.Commands.RefreshToken;
|
using Features.Auth.Commands.RefreshToken;
|
||||||
using Features.Auth.Dtos;
|
using Features.Auth.Dtos;
|
||||||
using Features.Auth.Services;
|
using Features.Auth.Services;
|
||||||
@@ -15,11 +14,12 @@ public class RefreshTokenHandlerTests
|
|||||||
Mock<ITokenService> tokenServiceMock = new();
|
Mock<ITokenService> tokenServiceMock = new();
|
||||||
RefreshTokenHandler handler = new(tokenServiceMock.Object);
|
RefreshTokenHandler handler = new(tokenServiceMock.Object);
|
||||||
Guid userId = Guid.NewGuid();
|
Guid userId = Guid.NewGuid();
|
||||||
UserAccount user = new() { UserAccountId = userId, Username = "testuser" };
|
|
||||||
|
|
||||||
tokenServiceMock
|
tokenServiceMock
|
||||||
.Setup(x => x.RefreshTokenAsync("old-refresh-token"))
|
.Setup(x => x.RefreshTokenAsync("old-refresh-token"))
|
||||||
.ReturnsAsync(new RefreshTokenResult(user, "new-refresh-token", "new-access-token"));
|
.ReturnsAsync(
|
||||||
|
new RefreshTokenResult(userId, "testuser", "new-refresh-token", "new-access-token")
|
||||||
|
);
|
||||||
|
|
||||||
LoginPayload result = await handler.Handle(
|
LoginPayload result = await handler.Handle(
|
||||||
new RefreshTokenCommand("old-refresh-token"),
|
new RefreshTokenCommand("old-refresh-token"),
|
||||||
|
|||||||
@@ -1,25 +1,26 @@
|
|||||||
using System.IdentityModel.Tokens.Jwt;
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using Domain.Entities;
|
|
||||||
using Domain.Exceptions;
|
using Domain.Exceptions;
|
||||||
using Features.Auth.Repository;
|
using Features.Auth.Identity;
|
||||||
using Features.Auth.Services;
|
using Features.Auth.Services;
|
||||||
|
using Features.Auth.Tests.TestSupport;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Infrastructure.Jwt;
|
using Infrastructure.Jwt;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Moq;
|
using Moq;
|
||||||
|
|
||||||
namespace Features.Auth.Tests.Services;
|
namespace Features.Auth.Tests.Services;
|
||||||
|
|
||||||
public class TokenServiceRefreshTests
|
public class TokenServiceRefreshTests
|
||||||
{
|
{
|
||||||
private readonly Mock<IAuthRepository> _authRepositoryMock;
|
|
||||||
private readonly Mock<ITokenInfrastructure> _tokenInfraMock;
|
private readonly Mock<ITokenInfrastructure> _tokenInfraMock;
|
||||||
private readonly TokenService _tokenService;
|
private readonly TokenService _tokenService;
|
||||||
|
private readonly Mock<UserManager<ApplicationUser>> _userManagerMock;
|
||||||
|
|
||||||
public TokenServiceRefreshTests()
|
public TokenServiceRefreshTests()
|
||||||
{
|
{
|
||||||
_tokenInfraMock = new Mock<ITokenInfrastructure>();
|
_tokenInfraMock = new Mock<ITokenInfrastructure>();
|
||||||
_authRepositoryMock = new Mock<IAuthRepository>();
|
_userManagerMock = UserManagerMockFactory.Create();
|
||||||
|
|
||||||
// Set environment variables for tokens
|
// Set environment variables for tokens
|
||||||
Environment.SetEnvironmentVariable(
|
Environment.SetEnvironmentVariable(
|
||||||
@@ -35,7 +36,7 @@ public class TokenServiceRefreshTests
|
|||||||
"test-confirmation-secret-that-is-very-long-1234567890"
|
"test-confirmation-secret-that-is-very-long-1234567890"
|
||||||
);
|
);
|
||||||
|
|
||||||
_tokenService = new TokenService(_tokenInfraMock.Object, _authRepositoryMock.Object);
|
_tokenService = new TokenService(_tokenInfraMock.Object, _userManagerMock.Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -56,15 +57,7 @@ public class TokenServiceRefreshTests
|
|||||||
ClaimsIdentity claimsIdentity = new(claims);
|
ClaimsIdentity claimsIdentity = new(claims);
|
||||||
ClaimsPrincipal principal = new(claimsIdentity);
|
ClaimsPrincipal principal = new(claimsIdentity);
|
||||||
|
|
||||||
UserAccount userAccount = new()
|
ApplicationUser user = new() { Id = userId, UserName = username };
|
||||||
{
|
|
||||||
UserAccountId = userId,
|
|
||||||
Username = username,
|
|
||||||
FirstName = "Test",
|
|
||||||
LastName = "User",
|
|
||||||
Email = "test@example.com",
|
|
||||||
DateOfBirth = new DateTime(1990, 1, 1),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Mock the validation of refresh token
|
// Mock the validation of refresh token
|
||||||
_tokenInfraMock
|
_tokenInfraMock
|
||||||
@@ -78,19 +71,19 @@ public class TokenServiceRefreshTests
|
|||||||
(Guid _, string _, DateTime _, string _) => $"generated-token-{Guid.NewGuid()}"
|
(Guid _, string _, DateTime _, string _) => $"generated-token-{Guid.NewGuid()}"
|
||||||
);
|
);
|
||||||
|
|
||||||
_authRepositoryMock.Setup(x => x.GetUserByIdAsync(userId)).ReturnsAsync(userAccount);
|
_userManagerMock.Setup(x => x.FindByIdAsync(userId.ToString())).ReturnsAsync(user);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
RefreshTokenResult result = await _tokenService.RefreshTokenAsync(refreshToken);
|
RefreshTokenResult result = await _tokenService.RefreshTokenAsync(refreshToken);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
result.Should().NotBeNull();
|
result.Should().NotBeNull();
|
||||||
result.UserAccount.UserAccountId.Should().Be(userId);
|
result.UserId.Should().Be(userId);
|
||||||
result.UserAccount.Username.Should().Be(username);
|
result.Username.Should().Be(username);
|
||||||
result.AccessToken.Should().NotBeEmpty();
|
result.AccessToken.Should().NotBeEmpty();
|
||||||
result.RefreshToken.Should().NotBeEmpty();
|
result.RefreshToken.Should().NotBeEmpty();
|
||||||
|
|
||||||
_authRepositoryMock.Verify(x => x.GetUserByIdAsync(userId), Times.Once);
|
_userManagerMock.Verify(x => x.FindByIdAsync(userId.ToString()), Times.Once);
|
||||||
|
|
||||||
// Verify tokens were generated (called twice - once for access, once for refresh)
|
// Verify tokens were generated (called twice - once for access, once for refresh)
|
||||||
_tokenInfraMock.Verify(
|
_tokenInfraMock.Verify(
|
||||||
@@ -161,7 +154,9 @@ public class TokenServiceRefreshTests
|
|||||||
.Setup(x => x.ValidateJwtAsync(refreshToken, It.IsAny<string>()))
|
.Setup(x => x.ValidateJwtAsync(refreshToken, It.IsAny<string>()))
|
||||||
.ReturnsAsync(principal);
|
.ReturnsAsync(principal);
|
||||||
|
|
||||||
_authRepositoryMock.Setup(x => x.GetUserByIdAsync(userId)).ReturnsAsync((UserAccount?)null);
|
_userManagerMock
|
||||||
|
.Setup(x => x.FindByIdAsync(userId.ToString()))
|
||||||
|
.ReturnsAsync((ApplicationUser?)null);
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
await FluentActions
|
await FluentActions
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
using System.IdentityModel.Tokens.Jwt;
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using Domain.Exceptions;
|
using Domain.Exceptions;
|
||||||
using Features.Auth.Repository;
|
|
||||||
using Features.Auth.Services;
|
using Features.Auth.Services;
|
||||||
|
using Features.Auth.Tests.TestSupport;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Infrastructure.Jwt;
|
using Infrastructure.Jwt;
|
||||||
using Moq;
|
using Moq;
|
||||||
@@ -32,7 +32,10 @@ public class TokenServiceValidationTests
|
|||||||
"test-confirmation-secret-that-is-very-long-1234567890"
|
"test-confirmation-secret-that-is-very-long-1234567890"
|
||||||
);
|
);
|
||||||
|
|
||||||
_tokenService = new TokenService(_tokenInfraMock.Object, new Mock<IAuthRepository>().Object);
|
_tokenService = new TokenService(
|
||||||
|
_tokenInfraMock.Object,
|
||||||
|
UserManagerMockFactory.Create().Object
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ public class RefreshTokenHandler(ITokenService tokenService)
|
|||||||
{
|
{
|
||||||
RefreshTokenResult result = await tokenService.RefreshTokenAsync(request.RefreshToken);
|
RefreshTokenResult result = await tokenService.RefreshTokenAsync(request.RefreshToken);
|
||||||
return new LoginPayload(
|
return new LoginPayload(
|
||||||
result.UserAccount.UserAccountId,
|
result.UserId,
|
||||||
result.UserAccount.Username,
|
result.Username,
|
||||||
result.RefreshToken,
|
result.RefreshToken,
|
||||||
result.AccessToken
|
result.AccessToken
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using Domain.Entities;
|
|
||||||
|
|
||||||
namespace Features.Auth.Services;
|
namespace Features.Auth.Services;
|
||||||
|
|
||||||
@@ -22,7 +21,7 @@ public enum TokenType
|
|||||||
public record ValidatedToken(Guid UserId, string Username, ClaimsPrincipal Principal);
|
public record ValidatedToken(Guid UserId, string Username, ClaimsPrincipal Principal);
|
||||||
|
|
||||||
/// <summary>Represents the result of refreshing a user's session.</summary>
|
/// <summary>Represents the result of refreshing a user's session.</summary>
|
||||||
public record RefreshTokenResult(UserAccount UserAccount, string RefreshToken, string AccessToken);
|
public record RefreshTokenResult(Guid UserId, string Username, string RefreshToken, string AccessToken);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Defines the expiration windows, in hours, for each type of token issued by <see cref="ITokenService" />.
|
/// Defines the expiration windows, in hours, for each type of token issued by <see cref="ITokenService" />.
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
using System.IdentityModel.Tokens.Jwt;
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using Domain.Entities;
|
|
||||||
using Domain.Exceptions;
|
using Domain.Exceptions;
|
||||||
using Features.Auth.Repository;
|
using Features.Auth.Identity;
|
||||||
using Infrastructure.Jwt;
|
using Infrastructure.Jwt;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
namespace Features.Auth.Services;
|
namespace Features.Auth.Services;
|
||||||
|
|
||||||
@@ -14,20 +14,23 @@ namespace Features.Auth.Services;
|
|||||||
public class TokenService : ITokenService
|
public class TokenService : ITokenService
|
||||||
{
|
{
|
||||||
private readonly string _accessTokenSecret;
|
private readonly string _accessTokenSecret;
|
||||||
private readonly IAuthRepository _authRepository;
|
|
||||||
private readonly string _confirmationTokenSecret;
|
private readonly string _confirmationTokenSecret;
|
||||||
private readonly string _refreshTokenSecret;
|
private readonly string _refreshTokenSecret;
|
||||||
private readonly ITokenInfrastructure _tokenInfrastructure;
|
private readonly ITokenInfrastructure _tokenInfrastructure;
|
||||||
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
|
|
||||||
/// <summary>Initializes a new instance of the <see cref="TokenService" /> class.</summary>
|
/// <summary>Initializes a new instance of the <see cref="TokenService" /> class.</summary>
|
||||||
/// <exception cref="InvalidOperationException">
|
/// <exception cref="InvalidOperationException">
|
||||||
/// Thrown when any of the <c>ACCESS_TOKEN_SECRET</c>, <c>REFRESH_TOKEN_SECRET</c>, or
|
/// Thrown when any of the <c>ACCESS_TOKEN_SECRET</c>, <c>REFRESH_TOKEN_SECRET</c>, or
|
||||||
/// <c>CONFIRMATION_TOKEN_SECRET</c> environment variables are not set.
|
/// <c>CONFIRMATION_TOKEN_SECRET</c> environment variables are not set.
|
||||||
/// </exception>
|
/// </exception>
|
||||||
public TokenService(ITokenInfrastructure tokenInfrastructure, IAuthRepository authRepository)
|
public TokenService(
|
||||||
|
ITokenInfrastructure tokenInfrastructure,
|
||||||
|
UserManager<ApplicationUser> userManager
|
||||||
|
)
|
||||||
{
|
{
|
||||||
_tokenInfrastructure = tokenInfrastructure;
|
_tokenInfrastructure = tokenInfrastructure;
|
||||||
_authRepository = authRepository;
|
_userManager = userManager;
|
||||||
|
|
||||||
_accessTokenSecret =
|
_accessTokenSecret =
|
||||||
Environment.GetEnvironmentVariable("ACCESS_TOKEN_SECRET")
|
Environment.GetEnvironmentVariable("ACCESS_TOKEN_SECRET")
|
||||||
@@ -100,14 +103,14 @@ public class TokenService : ITokenService
|
|||||||
public async Task<RefreshTokenResult> RefreshTokenAsync(string refreshTokenString)
|
public async Task<RefreshTokenResult> RefreshTokenAsync(string refreshTokenString)
|
||||||
{
|
{
|
||||||
ValidatedToken validated = await ValidateRefreshTokenAsync(refreshTokenString);
|
ValidatedToken validated = await ValidateRefreshTokenAsync(refreshTokenString);
|
||||||
UserAccount? user = await _authRepository.GetUserByIdAsync(validated.UserId);
|
ApplicationUser? user = await _userManager.FindByIdAsync(validated.UserId.ToString());
|
||||||
if (user == null)
|
if (user == null)
|
||||||
throw new UnauthorizedException("User account not found");
|
throw new UnauthorizedException("User account not found");
|
||||||
|
|
||||||
string newAccess = GenerateAccessToken(user.UserAccountId, user.Username);
|
string newAccess = GenerateAccessToken(user.Id, user.UserName);
|
||||||
string newRefresh = GenerateRefreshToken(user.UserAccountId, user.Username);
|
string newRefresh = GenerateRefreshToken(user.Id, user.UserName);
|
||||||
|
|
||||||
return new RefreshTokenResult(user, newRefresh, newAccess);
|
return new RefreshTokenResult(user.Id, user.UserName, newRefresh, newAccess);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
Reference in New Issue
Block a user