Fix topology auth: pre-auth middleware reads gateway identity envelope

The identity envelope PostConfigure on JwtBearerOptions didn't work because
AddStellaOpsResourceServerAuthentication configures its own events that
override PostConfigure. The OnMessageReceived handler was only in the
TestSigningSecret branch, never in the OIDC discovery branch used in prod.

Fix: Add a middleware BEFORE UseAuthentication() that reads
X-StellaOps-Identity-Envelope headers, verifies HMAC-SHA256 signature
using Router:IdentityEnvelopeSigningKey (from router-microservice-defaults),
and sets HttpContext.User with claims from the envelope.

Also fixed: read signing key from Router:IdentityEnvelopeSigningKey config
path (matches the compose env var Router__IdentityEnvelopeSigningKey from
x-router-microservice-defaults).

Verified: Topology wizard "Create Region" now succeeds — Next button enables.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-03-16 09:39:46 +02:00
parent ed6cd76c62
commit 3577c268a4

View File

@@ -785,6 +785,41 @@ if (authorityConfigured)
resourceOptions.BypassNetworks.Add(network);
}
});
// Add identity envelope fallback for ReverseProxy routes (OIDC branch).
// When the gateway proxies a request via ReverseProxy, it strips the Authorization header
// and attaches signed X-StellaOps-Identity-Envelope headers. This handler reads those
// headers and converts them to a ClaimsPrincipal when no JWT token is present.
builder.Services.PostConfigure<JwtBearerOptions>(
StellaOpsAuthenticationDefaults.AuthenticationScheme,
jwtOptions =>
{
var existingOnMessageReceived = jwtOptions.Events?.OnMessageReceived;
jwtOptions.Events ??= new JwtBearerEvents();
jwtOptions.Events.OnMessageReceived = async context =>
{
if (existingOnMessageReceived is not null)
{
await existingOnMessageReceived(context);
}
// If JWT handler already found a token or succeeded, skip envelope check
if (!string.IsNullOrWhiteSpace(context.Token) || context.Result?.Succeeded == true)
{
return;
}
var logger = context.HttpContext.RequestServices
.GetRequiredService<ILoggerFactory>()
.CreateLogger("Concelier.IdentityEnvelope");
if (TryAuthenticateFromIdentityEnvelope(context.HttpContext, logger))
{
context.Principal = context.HttpContext.User;
context.Success();
}
};
});
}
else
{
@@ -850,8 +885,10 @@ if (authorityConfigured)
// No JWT token — check for gateway identity envelope (ReverseProxy passthrough)
if (TryAuthenticateFromIdentityEnvelope(context.HttpContext, logger))
{
// Envelope authentication succeeded — skip JWT validation
context.NoResult();
// Envelope authentication succeeded — set the principal and mark as handled.
// Use context.Principal + context.Success() so the JwtBearer handler
// reports success without trying to validate a JWT.
context.Principal = context.HttpContext.User;
context.Success();
return Task.CompletedTask;
}
@@ -1006,6 +1043,20 @@ app.UseStellaOpsLocalization();
if (authorityConfigured)
{
// Identity envelope middleware: authenticate ReverseProxy requests from the gateway.
// Must run BEFORE UseAuthentication so the principal is set before JwtBearer evaluates.
app.Use(async (context, next) =>
{
if (context.User?.Identity?.IsAuthenticated != true)
{
var envelopeLogger = context.RequestServices
.GetRequiredService<ILoggerFactory>()
.CreateLogger("Concelier.IdentityEnvelope");
TryAuthenticateFromIdentityEnvelope(context, envelopeLogger);
}
await next();
});
app.UseAuthentication();
// Middleware to log authorization denied results (BEFORE UseAuthorization so it wraps around it)
@@ -3627,8 +3678,8 @@ static bool TryAuthenticateFromIdentityEnvelope(HttpContext httpContext, Microso
return false;
}
var signingKey = httpContext.RequestServices.GetService<IConfiguration>()
?.GetValue<string>("STELLAOPS_IDENTITY_ENVELOPE_SIGNING_KEY")
var config = httpContext.RequestServices.GetService<IConfiguration>();
var signingKey = config?.GetValue<string>("Router:IdentityEnvelopeSigningKey")
?? Environment.GetEnvironmentVariable("STELLAOPS_IDENTITY_ENVELOPE_SIGNING_KEY");
if (string.IsNullOrWhiteSpace(signingKey))