using w4c_workflows.Services; namespace w4c_workflows.Middleware; /// /// Two auth surfaces for the control plane: /// - /api/keys* → the tenant's main JWT (shared Auth:JwtSigningKey). Lets a /// logged-in tenant mint/rotate/revoke its operator keys. /// - everything else → per-tenant operator key (Authorization: Bearer ). /// Used by both the frontend and the per-tenant worker. /// Resolved identity is exposed via HttpContext.Items: TenantId, Scopes, /// AuthKind ("jwt" | "operator"). /// public class AuthMiddleware { private static readonly PathString[] PublicPaths = { new("/health"), new("/health/ready"), new("/api/about"), // self-info: name/version/multiTenant, no operator key needed new("/openapi"), new("/scalar"), // interactive API explorer (Scalar) — no operator key needed new("/api/scalar"), // Scalar reference exposed under /api/scalar/ — no operator key needed new("/h"), // webhook receiver (external callers have no operator key) }; private readonly RequestDelegate _next; private readonly string _signingKey; private readonly string? _jwtIssuer; private readonly string? _jwtAudience; private readonly bool _enforceScopes; private readonly ILogger _logger; public AuthMiddleware(RequestDelegate next, IConfiguration config, ILogger logger) { _next = next; _signingKey = config["Auth:JwtSigningKey"] ?? string.Empty; _jwtIssuer = config["Auth:JwtIssuer"]; _jwtAudience = config["Auth:JwtAudience"]; // Fail-closed switch for operator keys that carry no scope set. Off by // default so legacy/seed keys keep working until they are re-minted. _enforceScopes = config.GetValue("Auth:EnforceScopes", false); _logger = logger; } public async Task InvokeAsync(HttpContext context) { var path = context.Request.Path; if (IsPublic(path)) { await _next(context); return; } // /api/keys* — main JWT surface. if (path.StartsWithSegments("/api/keys")) { var principal = JwtValidator.Validate( context.Request.Headers.Authorization.ToString(), _signingKey, _jwtIssuer, _jwtAudience); var tenantId = principal?.FindFirst("tenant_id")?.Value; if (principal == null || string.IsNullOrEmpty(tenantId)) { await Unauthorized(context, "Valid main JWT required for key management"); return; } context.Items["TenantId"] = tenantId; context.Items["AuthKind"] = "jwt"; await _next(context); return; } // The rich HTML preview endpoint (/api/workflows/{id}/html) is reached // from an