using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; namespace KArtSell.Host.Security; /// /// JWT Token-based authentication for production. /// Validates Bearer tokens from Authorization header. /// public sealed class JwtAuthenticationHandler( IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder) : AuthenticationHandler(options, logger, encoder) { private static readonly JwtSecurityTokenHandler TokenHandler = new(); protected override Task HandleAuthenticateAsync() { try { // Extract Bearer token from Authorization header var authHeader = Request.Headers.Authorization.ToString(); if (string.IsNullOrWhiteSpace(authHeader) || !authHeader.StartsWith("Bearer ", StringComparison.Ordinal)) { return Task.FromResult(AuthenticateResult.NoResult()); } var token = authHeader["Bearer ".Length..]; // Validate token var validationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(Options.JwtKey)), ValidateIssuer = true, ValidIssuer = Options.JwtIssuer, ValidateAudience = true, ValidAudience = Options.JwtAudience, ValidateLifetime = true, ClockSkew = TimeSpan.FromSeconds(Options.ClockSkewSeconds) }; var principal = TokenHandler.ValidateToken(token, validationParameters, out _); var ticket = new AuthenticationTicket(principal, Scheme.Name); return Task.FromResult(AuthenticateResult.Success(ticket)); } catch (Exception ex) { Logger.LogWarning("JWT validation failed: {Message}", ex.Message); return Task.FromResult(AuthenticateResult.Fail("Invalid token")); } } } /// /// Configuration options for JWT authentication. /// public class JwtAuthenticationOptions : AuthenticationSchemeOptions { public string JwtKey { get; set; } = string.Empty; public string JwtIssuer { get; set; } = string.Empty; public string JwtAudience { get; set; } = string.Empty; public int ExpirationMinutes { get; set; } = 60; public int ClockSkewSeconds { get; set; } = 30; }