using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using Domain.Exceptions; using Features.Users.Identity; using Configuration; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; namespace Features.Users.Services; /// /// Default implementation of that generates and validates JWTs /// for access, refresh, and confirmation flows using secrets read from configuration. /// public class TokenService : ITokenService { private readonly string _accessTokenSecret; private readonly string _confirmationTokenSecret; private readonly string _refreshTokenSecret; private readonly ITokenInfrastructure _tokenInfrastructure; private readonly UserManager _userManager; /// /// Thrown when any of the ACCESS_TOKEN_SECRET, REFRESH_TOKEN_SECRET, or /// CONFIRMATION_TOKEN_SECRET configuration values are not set. /// public TokenService( ITokenInfrastructure tokenInfrastructure, UserManager userManager, IConfiguration configuration ) { _tokenInfrastructure = tokenInfrastructure; _userManager = userManager; _accessTokenSecret = ConfigurationHelpers.GetKeyOrThrow( configuration, ConfigurationKeys.AccessTokenSecret ); _refreshTokenSecret = ConfigurationHelpers.GetKeyOrThrow( configuration, ConfigurationKeys.RefreshTokenSecret ); _confirmationTokenSecret = ConfigurationHelpers.GetKeyOrThrow( configuration, ConfigurationKeys.ConfirmationTokenSecret ); } /// public string GenerateAccessToken(Guid userId, string username) { DateTime expiresAt = DateTime.UtcNow.AddHours(TokenServiceExpirationHours.AccessTokenHours); return _tokenInfrastructure.GenerateJwt(userId, username, expiresAt, _accessTokenSecret); } /// public string GenerateRefreshToken(Guid userId, string username) { DateTime expiresAt = DateTime.UtcNow.AddHours( TokenServiceExpirationHours.RefreshTokenHours ); return _tokenInfrastructure.GenerateJwt(userId, username, expiresAt, _refreshTokenSecret); } /// public string GenerateConfirmationToken(Guid userId, string username) { DateTime expiresAt = DateTime.UtcNow.AddHours( TokenServiceExpirationHours.ConfirmationTokenHours ); return _tokenInfrastructure.GenerateJwt( userId, username, expiresAt, _confirmationTokenSecret ); } /// public async Task ValidateRefreshTokenAsync(string token) { return await ValidateTokenInternalAsync(token, _refreshTokenSecret, "refresh"); } /// public async Task ValidateConfirmationTokenAsync(string token) { return await ValidateTokenInternalAsync(token, _confirmationTokenSecret, "confirmation"); } /// public async Task RefreshTokenAsync(string refreshTokenString) { ValidatedToken validated = await ValidateRefreshTokenAsync(refreshTokenString); ApplicationUser? user = await _userManager.FindByIdAsync(validated.UserId.ToString()); if (user == null) throw new UnauthorizedException("User account not found"); string newAccess = GenerateAccessToken(user.Id, user.UserName); string newRefresh = GenerateRefreshToken(user.Id, user.UserName); return new RefreshTokenResult(user.Id, user.UserName, 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. /// /// A human-readable label (e.g. "access", "refresh", "confirmation") used in error messages. /// /// 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}"); } } }