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;
}