mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-04-05 10:09:03 +00:00
157 lines
6.0 KiB
C#
157 lines
6.0 KiB
C#
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;
|
|
|
|
public enum TokenType
|
|
{
|
|
AccessToken,
|
|
RefreshToken,
|
|
ConfirmationToken,
|
|
}
|
|
|
|
public record ValidatedToken(Guid UserId, string Username, ClaimsPrincipal Principal);
|
|
|
|
public record RefreshTokenResult(
|
|
UserAccount UserAccount,
|
|
string RefreshToken,
|
|
string AccessToken
|
|
);
|
|
|
|
public static class TokenServiceExpirationHours
|
|
{
|
|
public const double AccessTokenHours = 1;
|
|
public const double RefreshTokenHours = 504; // 21 days
|
|
public const double ConfirmationTokenHours = 0.5; // 30 minutes
|
|
}
|
|
|
|
public interface ITokenService
|
|
{
|
|
string GenerateAccessToken(UserAccount user);
|
|
string GenerateRefreshToken(UserAccount user);
|
|
string GenerateConfirmationToken(UserAccount user);
|
|
string GenerateToken<T>(UserAccount user) where T : struct, Enum;
|
|
Task<ValidatedToken> ValidateAccessTokenAsync(string token);
|
|
Task<ValidatedToken> ValidateRefreshTokenAsync(string token);
|
|
Task<ValidatedToken> ValidateConfirmationTokenAsync(string token);
|
|
Task<RefreshTokenResult> RefreshTokenAsync(string refreshTokenString);
|
|
}
|
|
|
|
public class TokenService : ITokenService
|
|
{
|
|
private readonly ITokenInfrastructure _tokenInfrastructure;
|
|
private readonly IAuthRepository _authRepository;
|
|
|
|
private readonly string _accessTokenSecret;
|
|
private readonly string _refreshTokenSecret;
|
|
private readonly string _confirmationTokenSecret;
|
|
|
|
public TokenService(
|
|
ITokenInfrastructure tokenInfrastructure,
|
|
IAuthRepository authRepository
|
|
)
|
|
{
|
|
_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 GenerateAccessToken(UserAccount user)
|
|
{
|
|
var expiresAt = DateTime.UtcNow.AddHours(TokenServiceExpirationHours.AccessTokenHours);
|
|
return _tokenInfrastructure.GenerateJwt(user.UserAccountId, user.Username, expiresAt, _accessTokenSecret);
|
|
}
|
|
|
|
public string GenerateRefreshToken(UserAccount user)
|
|
{
|
|
var expiresAt = DateTime.UtcNow.AddHours(TokenServiceExpirationHours.RefreshTokenHours);
|
|
return _tokenInfrastructure.GenerateJwt(user.UserAccountId, user.Username, expiresAt, _refreshTokenSecret);
|
|
}
|
|
|
|
public string GenerateConfirmationToken(UserAccount user)
|
|
{
|
|
var expiresAt = DateTime.UtcNow.AddHours(TokenServiceExpirationHours.ConfirmationTokenHours);
|
|
return _tokenInfrastructure.GenerateJwt(user.UserAccountId, user.Username, expiresAt, _confirmationTokenSecret);
|
|
}
|
|
|
|
public string GenerateToken<T>(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<ValidatedToken> ValidateAccessTokenAsync(string token)
|
|
=> await ValidateTokenInternalAsync(token, _accessTokenSecret, "access");
|
|
|
|
public async Task<ValidatedToken> ValidateRefreshTokenAsync(string token)
|
|
=> await ValidateTokenInternalAsync(token, _refreshTokenSecret, "refresh");
|
|
|
|
public async Task<ValidatedToken> ValidateConfirmationTokenAsync(string token)
|
|
=> await ValidateTokenInternalAsync(token, _confirmationTokenSecret, "confirmation");
|
|
|
|
private async Task<ValidatedToken> 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<RefreshTokenResult> 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);
|
|
}
|
|
}
|