Files
git.stella-ops.org/src/Router/StellaOps.Gateway.WebService/Authorization/AuthorizationMiddleware.cs

136 lines
4.4 KiB
C#

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<AuthorizationMiddleware> _logger;
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false
};
public AuthorizationMiddleware(
RequestDelegate next,
IEffectiveClaimsStore claimsStore,
ILogger<AuthorizationMiddleware> 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);
}