save changes
This commit is contained in:
@@ -39,13 +39,20 @@ public sealed class IdentityHeaderPolicyMiddleware
|
||||
"X-Stella-Project",
|
||||
"X-Stella-Actor",
|
||||
"X-Stella-Scopes",
|
||||
// Headers used by downstream services in header-based auth mode
|
||||
"X-Scopes",
|
||||
"X-Tenant-Id",
|
||||
// Raw claim headers (internal/legacy pass-through)
|
||||
"sub",
|
||||
"tid",
|
||||
"scope",
|
||||
"scp",
|
||||
"cnf",
|
||||
"cnf.jkt"
|
||||
"cnf.jkt",
|
||||
// Auth headers consumed by the gateway — strip before proxying
|
||||
// so backends trust identity headers instead of re-validating JWT.
|
||||
"Authorization",
|
||||
"DPoP"
|
||||
];
|
||||
|
||||
public IdentityHeaderPolicyMiddleware(
|
||||
@@ -91,8 +98,18 @@ public sealed class IdentityHeaderPolicyMiddleware
|
||||
|
||||
private void StripReservedHeaders(HttpContext context)
|
||||
{
|
||||
var preserveAuthHeaders = _options.JwtPassthroughPrefixes.Count > 0
|
||||
&& _options.JwtPassthroughPrefixes.Any(prefix =>
|
||||
context.Request.Path.StartsWithSegments(prefix, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
foreach (var header in ReservedHeaders)
|
||||
{
|
||||
// Preserve Authorization/DPoP for routes that need JWT pass-through
|
||||
if (preserveAuthHeaders && (header == "Authorization" || header == "DPoP"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (context.Request.Headers.ContainsKey(header))
|
||||
{
|
||||
_logger.LogDebug(
|
||||
@@ -114,7 +131,7 @@ public sealed class IdentityHeaderPolicyMiddleware
|
||||
// In AllowAnonymous mode the Gateway cannot validate identity claims.
|
||||
// Pass through the client-provided tenant so the upstream service
|
||||
// can validate it against the JWT's own tenant claim.
|
||||
var passThruTenant = !string.IsNullOrWhiteSpace(clientTenant) ? clientTenant.Trim() : null;
|
||||
var passThruTenant = !string.IsNullOrWhiteSpace(clientTenant) ? clientTenant.Trim() : "default";
|
||||
|
||||
return new IdentityContext
|
||||
{
|
||||
@@ -192,9 +209,37 @@ public sealed class IdentityHeaderPolicyMiddleware
|
||||
}
|
||||
}
|
||||
|
||||
// Expand coarse OIDC scopes to fine-grained service scopes.
|
||||
// This bridges the gap between Authority-registered scopes (e.g. "scheduler:read")
|
||||
// and the fine-grained scopes that downstream services expect (e.g. "scheduler.runs.read").
|
||||
ExpandCoarseScopes(scopes);
|
||||
|
||||
return scopes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Expands coarse OIDC scopes into fine-grained service scopes.
|
||||
/// Pattern: "{service}:{action}" expands to "{service}.{resource}.{action}" for known resources.
|
||||
/// </summary>
|
||||
private static void ExpandCoarseScopes(HashSet<string> scopes)
|
||||
{
|
||||
// scheduler:read -> scheduler.schedules.read, scheduler.runs.read
|
||||
// scheduler:operate -> scheduler.schedules.write, scheduler.runs.write, scheduler.runs.preview, scheduler.runs.manage
|
||||
if (scopes.Contains("scheduler:read"))
|
||||
{
|
||||
scopes.Add("scheduler.schedules.read");
|
||||
scopes.Add("scheduler.runs.read");
|
||||
}
|
||||
|
||||
if (scopes.Contains("scheduler:operate"))
|
||||
{
|
||||
scopes.Add("scheduler.schedules.write");
|
||||
scopes.Add("scheduler.runs.write");
|
||||
scopes.Add("scheduler.runs.preview");
|
||||
scopes.Add("scheduler.runs.manage");
|
||||
}
|
||||
}
|
||||
|
||||
private void StoreIdentityContext(HttpContext context, IdentityContext identity)
|
||||
{
|
||||
context.Items[GatewayContextKeys.IsAnonymous] = identity.IsAnonymous;
|
||||
@@ -248,6 +293,7 @@ public sealed class IdentityHeaderPolicyMiddleware
|
||||
if (!string.IsNullOrEmpty(identity.Tenant))
|
||||
{
|
||||
headers["X-StellaOps-Tenant"] = identity.Tenant;
|
||||
headers["X-Tenant-Id"] = identity.Tenant;
|
||||
if (_options.EnableLegacyHeaders)
|
||||
{
|
||||
headers["X-Stella-Tenant"] = identity.Tenant;
|
||||
@@ -270,6 +316,7 @@ public sealed class IdentityHeaderPolicyMiddleware
|
||||
var sortedScopes = identity.Scopes.OrderBy(s => s, StringComparer.Ordinal);
|
||||
var scopesValue = string.Join(" ", sortedScopes);
|
||||
headers["X-StellaOps-Scopes"] = scopesValue;
|
||||
headers["X-Scopes"] = scopesValue;
|
||||
if (_options.EnableLegacyHeaders)
|
||||
{
|
||||
headers["X-Stella-Scopes"] = scopesValue;
|
||||
@@ -279,6 +326,7 @@ public sealed class IdentityHeaderPolicyMiddleware
|
||||
{
|
||||
// Explicit empty scopes for anonymous to prevent ambiguity
|
||||
headers["X-StellaOps-Scopes"] = string.Empty;
|
||||
headers["X-Scopes"] = string.Empty;
|
||||
if (_options.EnableLegacyHeaders)
|
||||
{
|
||||
headers["X-Stella-Scopes"] = string.Empty;
|
||||
@@ -347,4 +395,13 @@ public sealed class IdentityHeaderPolicyOptions
|
||||
/// Default: false (forbidden for security).
|
||||
/// </summary>
|
||||
public bool AllowScopeHeaderOverride { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Route prefixes where Authorization and DPoP headers should be preserved
|
||||
/// (passed through to the upstream service) instead of stripped.
|
||||
/// Use this for upstream services that require JWT validation themselves
|
||||
/// (e.g., Authority admin API at /console).
|
||||
/// Default: empty (strip auth headers for all routes).
|
||||
/// </summary>
|
||||
public List<string> JwtPassthroughPrefixes { get; set; } = [];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user