using Api.Models; using Api.Models.DTOs; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; namespace Api.Services { public interface IJwtService { string GenerateToken(User user); ClaimsPrincipal? ValidateToken(string token); DateTime GetTokenExpiration(string token); } public class JwtService : IJwtService { private readonly JwtSettings _jwtSettings; private readonly ILogger _logger; private readonly SymmetricSecurityKey _key; public JwtService(IConfiguration configuration, ILogger logger) { _jwtSettings = configuration.GetSection("Jwt").Get() ?? throw new InvalidOperationException("JWT settings not configured"); _logger = logger; _key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.SecretKey)); } public string GenerateToken(User user) { try { var tokenHandler = new JwtSecurityTokenHandler(); var credentials = new SigningCredentials(_key, SecurityAlgorithms.HmacSha256); var claims = new List { new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), new Claim(ClaimTypes.Email, user.Email), new Claim("jti", Guid.NewGuid().ToString()), new Claim("iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64) }; // Add optional claims if available if (!string.IsNullOrEmpty(user.FirstName)) { claims.Add(new Claim(ClaimTypes.GivenName, user.FirstName)); } if (!string.IsNullOrEmpty(user.LastName)) { claims.Add(new Claim(ClaimTypes.Surname, user.LastName)); } // Add provider information var providers = user.OAuthProviders.Select(p => p.Provider.ToString()).ToList(); if (providers.Any()) { claims.Add(new Claim("providers", string.Join(",", providers))); } var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(claims), Expires = DateTime.UtcNow.AddMinutes(_jwtSettings.ExpirationMinutes), SigningCredentials = credentials, Issuer = _jwtSettings.Issuer, Audience = _jwtSettings.Audience }; var token = tokenHandler.CreateToken(tokenDescriptor); return tokenHandler.WriteToken(token); } catch (Exception ex) { _logger.LogError(ex, "Error generating JWT token for user {UserId}", user.Id); throw new InvalidOperationException("Failed to generate access token", ex); } } public ClaimsPrincipal? ValidateToken(string token) { try { var tokenHandler = new JwtSecurityTokenHandler(); var validationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = _key, ValidateIssuer = true, ValidIssuer = _jwtSettings.Issuer, ValidateAudience = true, ValidAudience = _jwtSettings.Audience, ValidateLifetime = true, ClockSkew = TimeSpan.FromMinutes(5) // Allow 5 minutes clock skew }; var principal = tokenHandler.ValidateToken(token, validationParameters, out var validatedToken); // Ensure the token is a JWT token with the correct algorithm if (validatedToken is not JwtSecurityToken jwtToken || !jwtToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase)) { return null; } return principal; } catch (SecurityTokenException ex) { _logger.LogWarning(ex, "Invalid token validation attempt"); return null; } catch (Exception ex) { _logger.LogError(ex, "Error validating JWT token"); return null; } } public DateTime GetTokenExpiration(string token) { try { var tokenHandler = new JwtSecurityTokenHandler(); var jsonToken = tokenHandler.ReadJwtToken(token); return jsonToken.ValidTo; } catch (Exception ex) { _logger.LogError(ex, "Error reading token expiration"); throw new InvalidOperationException("Invalid token format", ex); } } } }