using System.Security.Claims; using System.IdentityModel.Tokens.Jwt; 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 ITokenInfrastructure _tokenInfrastructure; private readonly IAuthRepository _authRepository; private readonly string _accessTokenSecret; private readonly string _refreshTokenSecret; private readonly string _confirmationTokenSecret; /// /// 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) { var 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) { var 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) { var 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"); 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"), }; } /// /// 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) => 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) => 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) => await ValidateTokenInternalAsync(token, _confirmationTokenSecret, "confirmation"); /// /// 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 { 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}"); } } /// /// 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) { 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); } }