using Fitness_core_BE.Models.IOs; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Security.Cryptography; namespace Fitness_core_BE.Helpers { public class JwtMiddleware { [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class CustomMiddlewareAttribute : Attribute, IAsyncActionFilter { public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { var config = context.HttpContext.RequestServices.GetService(); string token = null; var authHeader = context.HttpContext.Request.Headers["MobileAuthToken"].FirstOrDefault(); if (!string.IsNullOrWhiteSpace(authHeader) && authHeader.StartsWith("Bearer ")) { token = authHeader.Substring("Bearer ".Length).Trim(); } if (string.IsNullOrWhiteSpace(token)) { token = context.HttpContext.Request.Cookies["access_token"]; } if (string.IsNullOrWhiteSpace(token)) { context.HttpContext.Response.StatusCode = 401; await context.HttpContext.Response.WriteAsJsonAsync(new ApiResponse { StatusCode = 401, Message = "Authentication cookie is missing", Data = null }); return; } try { var tokenHandler = new JwtSecurityTokenHandler(); var xmlKey = config["Jwt:PublicKey"]; if (string.IsNullOrWhiteSpace(xmlKey)) throw new SecurityTokenException("JWT public key not configured."); // Use FromXmlString (same approach as the Auth service's RsaKeyProvider) // to ensure identical key loading behavior var rsa = RSA.Create(); rsa.FromXmlString(xmlKey); var rsaParameters = rsa.ExportParameters(false); // Create RsaSecurityKey from RSAParameters directly (not from RSA object) // to avoid object lifecycle/disposal issues with IdentityModel v8+ var key = new RsaSecurityKey(rsaParameters) { KeyId = ComputeKeyId(rsaParameters) }; var parameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = key, ValidateIssuer = true, ValidIssuer = config["Jwt:Issuer"], ValidateAudience = true, ValidAudience = config["Jwt:Audience"], ValidateLifetime = true, ClockSkew = TimeSpan.FromMinutes(2) }; var principal = tokenHandler.ValidateToken(token, parameters, out SecurityToken _); context.HttpContext.User = principal; context.HttpContext.Items["UserId"] = principal.FindFirst("UserId")?.Value; context.HttpContext.Items["Email"] = principal.FindFirst("Email")?.Value; context.HttpContext.Items["UserType"] = principal.FindFirst("UserType")?.Value; context.HttpContext.Items["RoleId"] = principal.FindFirst("RoleId")?.Value; } catch (Exception) { context.HttpContext.Response.StatusCode = 401; await context.HttpContext.Response.WriteAsJsonAsync(new ApiResponse { StatusCode = 401, Message = "Invalid or expired token in cookie", Data = null }); return; } await next(); } private static string ComputeKeyId(RSAParameters publicParameters) { var modulus = publicParameters.Modulus ?? Array.Empty(); var exponent = publicParameters.Exponent ?? Array.Empty(); var payload = $"{Convert.ToBase64String(modulus)}.{Convert.ToBase64String(exponent)}"; var hash = SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(payload)); return Base64UrlEncoder.Encode(hash); } } } }