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