Files
KArtSell.Aegis/src/KArtSell.Host/Security/JwtAuthenticationHandler.cs
T
kjh2064 6adbee03eb
deploy / deploy (push) Successful in 2m6s
deploy / notify (push) Successful in 1s
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>
2026-08-17 23:56:36 +09:00

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