feat: JWT Token-based authentication for production (Release mode)
- 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>
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user