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; /// /// Identifies the kind of token being generated or validated. /// public enum TokenType { /// A short-lived token used to authorize API requests. AccessToken, /// A long-lived token used to obtain new access tokens. RefreshToken, /// A short-lived token used to confirm a user's email/account. ConfirmationToken, } /// /// Represents the result of successfully validating a token. /// /// The unique identifier of the user the token belongs to. /// The username extracted from the token's claims. /// The produced from the validated token. public record ValidatedToken(Guid UserId, string Username, ClaimsPrincipal Principal); /// /// Represents the result of refreshing a user's session. /// /// The user account associated with the refreshed session. /// The newly issued refresh token. /// The newly issued access token. public record RefreshTokenResult( UserAccount UserAccount, string RefreshToken, string AccessToken ); /// /// Defines the expiration windows, in hours, for each type of token issued by . /// public static class TokenServiceExpirationHours { /// The expiration window, in hours, for access tokens. public const double AccessTokenHours = 1; /// The expiration window, in hours, for refresh tokens (21 days). public const double RefreshTokenHours = 504; // 21 days /// The expiration window, in hours, for confirmation tokens (30 minutes). public const double ConfirmationTokenHours = 0.5; // 30 minutes } /// /// Defines operations for generating and validating JWTs used for access, refresh, and account confirmation. /// public interface ITokenService { /// /// Generates a new access token for the given user. /// /// The user account to generate the token for. /// The signed access token string. string GenerateAccessToken(UserAccount user); /// /// Generates a new refresh token for the given user. /// /// The user account to generate the token for. /// The signed refresh token string. string GenerateRefreshToken(UserAccount user); /// /// Generates a new confirmation token for the given user. /// /// The user account to generate the token for. /// The signed confirmation token string. string GenerateConfirmationToken(UserAccount user); /// /// Generates a token of the type specified by , which must be . /// /// 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. string GenerateToken(UserAccount user) where T : struct, Enum; /// /// Validates an access token. /// /// The access token string to validate. /// A describing the token's claims. Task ValidateAccessTokenAsync(string token); /// /// Validates a refresh token. /// /// The refresh token string to validate. /// A describing the token's claims. Task ValidateRefreshTokenAsync(string token); /// /// Validates a confirmation token. /// /// The confirmation token string to validate. /// A describing the token's claims. Task ValidateConfirmationTokenAsync(string token); /// /// Validates a refresh token and issues a new access/refresh token pair for the associated user. /// /// The refresh token string to validate and exchange. /// A containing the user and newly issued tokens. Task RefreshTokenAsync(string refreshTokenString); } /// /// 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); } }