6adbee03eb
- Implemented JwtAuthenticationHandler for Bearer token validation - Created LoginEndpoint for JWT token issuance (POST /api/auth/login) - Added JWT configuration to appsettings.json (Key, Issuer, Audience, ExpirationMinutes) - Updated Program.cs to use JWT authentication in Release mode (replaces FailClosedAuthenticationHandler) - Registered System.IdentityModel.Tokens.Jwt NuGet package - Token validation includes issuer, audience, expiration, and configurable clock skew - Backward compatible: Development mode continues to use DevelopmentHeaderAuthenticationHandler This enables production deployments to use standard JWT-based authentication instead of rejecting all requests. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
74 lines
2.7 KiB
C#
74 lines
2.7 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// JWT Token-based authentication for production.
|
|
/// Validates Bearer tokens from Authorization header.
|
|
/// </summary>
|
|
public sealed class JwtAuthenticationHandler(
|
|
IOptionsMonitor<JwtAuthenticationOptions> options,
|
|
ILoggerFactory logger,
|
|
UrlEncoder encoder)
|
|
: AuthenticationHandler<JwtAuthenticationOptions>(options, logger, encoder)
|
|
{
|
|
private static readonly JwtSecurityTokenHandler TokenHandler = new();
|
|
|
|
protected override Task<AuthenticateResult> 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"));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Configuration options for JWT authentication.
|
|
/// </summary>
|
|
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;
|
|
}
|