using StellaOps.Router.Common.Models; using StellaOps.Router.Gateway; using System.Text.Json; namespace StellaOps.Gateway.WebService.Authorization; public sealed class AuthorizationMiddleware { private readonly RequestDelegate _next; private readonly IEffectiveClaimsStore _claimsStore; private readonly ILogger _logger; private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = false }; public AuthorizationMiddleware( RequestDelegate next, IEffectiveClaimsStore claimsStore, ILogger logger) { _next = next; _claimsStore = claimsStore; _logger = logger; } public async Task InvokeAsync(HttpContext context) { if (!context.Items.TryGetValue(RouterHttpContextKeys.EndpointDescriptor, out var endpointObj) || endpointObj is not EndpointDescriptor endpoint) { await _next(context); return; } if (endpoint.AllowAnonymous) { await _next(context); return; } var requiresAuthentication = EndpointAuthorizationSemantics.ResolveRequiresAuthentication(endpoint); var isAuthenticated = context.User.Identity?.IsAuthenticated == true; if (requiresAuthentication && !isAuthenticated) { _logger.LogWarning( "Authorization failed for {Method} {Path}: unauthenticated principal", endpoint.Method, endpoint.Path); await WriteUnauthorizedAsync(context, endpoint); return; } var effectiveClaims = _claimsStore.GetEffectiveClaims( endpoint.ServiceName, endpoint.Method, endpoint.Path); if (effectiveClaims.Count == 0) { await _next(context); return; } foreach (var required in effectiveClaims) { var userClaims = context.User.Claims; var hasClaim = required.Value == null ? userClaims.Any(c => c.Type == required.Type) : userClaims.Any(c => c.Type == required.Type && c.Value == required.Value); if (!hasClaim) { _logger.LogWarning( "Authorization failed for {Method} {Path}: user lacks claim {ClaimType}={ClaimValue}", endpoint.Method, endpoint.Path, required.Type, required.Value ?? "(any)"); await WriteForbiddenAsync(context, endpoint, required); return; } } await _next(context); } private static Task WriteUnauthorizedAsync(HttpContext context, EndpointDescriptor endpoint) { context.Response.StatusCode = StatusCodes.Status401Unauthorized; context.Response.ContentType = "application/json; charset=utf-8"; var payload = new AuthorizationFailureResponse( Error: "unauthorized", Message: "Authentication required", RequiredClaimType: string.Empty, RequiredClaimValue: null, Service: endpoint.ServiceName, Version: endpoint.Version); return JsonSerializer.SerializeAsync(context.Response.Body, payload, JsonOptions, context.RequestAborted); } private static Task WriteForbiddenAsync( HttpContext context, EndpointDescriptor endpoint, ClaimRequirement required) { context.Response.StatusCode = StatusCodes.Status403Forbidden; context.Response.ContentType = "application/json; charset=utf-8"; var payload = new AuthorizationFailureResponse( Error: "forbidden", Message: "Authorization failed: missing required claim", RequiredClaimType: required.Type, RequiredClaimValue: required.Value, Service: endpoint.ServiceName, Version: endpoint.Version); return JsonSerializer.SerializeAsync(context.Response.Body, payload, JsonOptions, context.RequestAborted); } private sealed record AuthorizationFailureResponse( string Error, string Message, string RequiredClaimType, string? RequiredClaimValue, string Service, string Version); }