using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Security.Cryptography; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; namespace Fitness_core_BE.Auth { public static class UserJwtDefaults { public const string AuthenticationScheme = "UserJwtCookie"; } public sealed class UserJwtAuthenticationOptions : AuthenticationSchemeOptions { } /// /// Authenticates browser SignalR connections from the existing httpOnly /// access_token cookie. MVC controllers retain their existing action filter. /// public sealed class UserJwtAuthenticationHandler : AuthenticationHandler { private readonly IConfiguration _configuration; public UserJwtAuthenticationHandler( IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, IConfiguration configuration) : base(options, logger, encoder) { _configuration = configuration; } protected override Task HandleAuthenticateAsync() { var token = Request.Cookies["access_token"]; if (string.IsNullOrWhiteSpace(token)) return Task.FromResult(AuthenticateResult.NoResult()); try { var xmlKey = _configuration["Jwt:PublicKey"]; if (string.IsNullOrWhiteSpace(xmlKey)) throw new SecurityTokenException("JWT public key is not configured."); using var rsa = RSA.Create(); rsa.FromXmlString(xmlKey); var parameters = rsa.ExportParameters(false); var key = new RsaSecurityKey(parameters) { KeyId = ComputeKeyId(parameters) }; var principal = new JwtSecurityTokenHandler().ValidateToken( token, new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = key, ValidateIssuer = true, ValidIssuer = _configuration["Jwt:Issuer"], ValidateAudience = true, ValidAudience = _configuration["Jwt:Audience"], ValidateLifetime = true, ClockSkew = TimeSpan.FromMinutes(2) }, out _); var identity = new ClaimsIdentity( principal.Claims, UserJwtDefaults.AuthenticationScheme); var ticket = new AuthenticationTicket( new ClaimsPrincipal(identity), UserJwtDefaults.AuthenticationScheme); return Task.FromResult(AuthenticateResult.Success(ticket)); } catch (Exception ex) { return Task.FromResult( AuthenticateResult.Fail($"Invalid access token: {ex.Message}")); } } private static string ComputeKeyId(RSAParameters parameters) { var modulus = parameters.Modulus ?? Array.Empty(); var exponent = parameters.Exponent ?? Array.Empty(); var material = $"{Convert.ToBase64String(modulus)}.{Convert.ToBase64String(exponent)}"; return Base64UrlEncoder.Encode( SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(material))); } } }