using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Domain.Entities;
using Domain.Exceptions;
using Features.Auth.Repository;
using Infrastructure.Jwt;
namespace Features.Auth.Services;
///
/// Default implementation of that generates and validates JWTs
/// for access, refresh, and confirmation flows using secrets read from environment variables.
///
public class TokenService : ITokenService
{
private readonly string _accessTokenSecret;
private readonly IAuthRepository _authRepository;
private readonly string _confirmationTokenSecret;
private readonly string _refreshTokenSecret;
private readonly ITokenInfrastructure _tokenInfrastructure;
///
/// Initializes a new instance of , loading the access, refresh, and
/// confirmation token signing secrets from environment variables.
///
/// The infrastructure component used to generate and validate JWTs.
/// Repository used to look up user accounts during token refresh.
///
/// Thrown when any of the ACCESS_TOKEN_SECRET, REFRESH_TOKEN_SECRET, or
/// CONFIRMATION_TOKEN_SECRET environment variables are not set.
///
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");
}
///
/// Generates an access token for the given user, signed with the access token secret and
/// expiring after hours.
///
/// The user account to generate the token for.
/// The signed access token string.
public string GenerateAccessToken(UserAccount user)
{
DateTime expiresAt = DateTime.UtcNow.AddHours(TokenServiceExpirationHours.AccessTokenHours);
return _tokenInfrastructure.GenerateJwt(user.UserAccountId, user.Username, expiresAt, _accessTokenSecret);
}
///
/// Generates a refresh token for the given user, signed with the refresh token secret and
/// expiring after hours.
///
/// The user account to generate the token for.
/// The signed refresh token string.
public string GenerateRefreshToken(UserAccount user)
{
DateTime expiresAt = DateTime.UtcNow.AddHours(TokenServiceExpirationHours.RefreshTokenHours);
return _tokenInfrastructure.GenerateJwt(user.UserAccountId, user.Username, expiresAt, _refreshTokenSecret);
}
///
/// Generates a confirmation token for the given user, signed with the confirmation token secret and
/// expiring after hours.
///
/// The user account to generate the token for.
/// The signed confirmation token string.
public string GenerateConfirmationToken(UserAccount user)
{
DateTime expiresAt = DateTime.UtcNow.AddHours(TokenServiceExpirationHours.ConfirmationTokenHours);
return _tokenInfrastructure.GenerateJwt(user.UserAccountId, user.Username, expiresAt, _confirmationTokenSecret);
}
///
/// Generates a token of the kind specified by , dispatching to
/// , , or
/// based on the corresponding value.
///
/// The enum type identifying which kind of token to generate. Must be .
/// The user account to generate the token for.
/// The signed token string corresponding to the requested token type.
///
/// Thrown when is not or does not resolve to a known token type.
///
public string GenerateToken(UserAccount user) where T : struct, Enum
{
if (typeof(T) != typeof(TokenType))
throw new InvalidOperationException("Invalid token type");
string tokenTypeName = typeof(T).Name;
if (!Enum.TryParse(typeof(TokenType), tokenTypeName, out object? parsed))
throw new InvalidOperationException("Invalid token type");
TokenType tokenType = (TokenType)parsed;
return tokenType switch
{
TokenType.AccessToken => GenerateAccessToken(user),
TokenType.RefreshToken => GenerateRefreshToken(user),
TokenType.ConfirmationToken => GenerateConfirmationToken(user),
_ => throw new InvalidOperationException("Invalid token type")
};
}
///
/// Validates an access token against the access token secret.
///
/// The access token string to validate.
/// A describing the token's claims.
///
/// Thrown when the token is missing required claims, has a malformed user ID, or otherwise fails validation.
///
public async Task ValidateAccessTokenAsync(string token)
{
return await ValidateTokenInternalAsync(token, _accessTokenSecret, "access");
}
///
/// Validates a refresh token against the refresh token secret.
///
/// The refresh token string to validate.
/// A describing the token's claims.
///
/// Thrown when the token is missing required claims, has a malformed user ID, or otherwise fails validation.
///
public async Task ValidateRefreshTokenAsync(string token)
{
return await ValidateTokenInternalAsync(token, _refreshTokenSecret, "refresh");
}
///
/// Validates a confirmation token against the confirmation token secret.
///
/// The confirmation token string to validate.
/// A describing the token's claims.
///
/// Thrown when the token is missing required claims, has a malformed user ID, or otherwise fails validation.
///
public async Task ValidateConfirmationTokenAsync(string token)
{
return await ValidateTokenInternalAsync(token, _confirmationTokenSecret, "confirmation");
}
///
/// Validates the given refresh token, looks up the associated user, and issues a fresh
/// access/refresh token pair.
///
/// The refresh token string to validate and exchange.
/// A containing the user and newly issued tokens.
///
/// Thrown when the refresh token is invalid, or when the user account it refers to no longer exists.
///
public async Task RefreshTokenAsync(string refreshTokenString)
{
ValidatedToken validated = await ValidateRefreshTokenAsync(refreshTokenString);
UserAccount? user = await _authRepository.GetUserByIdAsync(validated.UserId);
if (user == null)
throw new UnauthorizedException("User account not found");
string newAccess = GenerateAccessToken(user);
string newRefresh = GenerateRefreshToken(user);
return new RefreshTokenResult(user, newRefresh, newAccess);
}
///
/// Performs the shared validation logic for access, refresh, and confirmation tokens:
/// validates the JWT signature/expiration, then extracts and parses the user ID and username claims.
///
/// The token string to validate.
/// The secret key to validate the token's signature against.
/// A human-readable label (e.g. "access", "refresh", "confirmation") used in error messages.
/// A describing the token's claims.
///
/// Thrown when required claims are missing, the user ID claim is not a valid ,
/// or the underlying token validation fails for any other reason (e.g. expired or invalid signature).
///
private async Task ValidateTokenInternalAsync(string token, string secret, string tokenType)
{
try
{
ClaimsPrincipal principal = await _tokenInfrastructure.ValidateJwtAsync(token, secret);
string? userIdClaim = principal.FindFirst(JwtRegisteredClaimNames.Sub)?.Value;
string? 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 Guid 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}");
}
}
}