From 3577c268a4b5c5fe876bc57831f570435e3f6415 Mon Sep 17 00:00:00 2001 From: master <> Date: Mon, 16 Mar 2026 09:39:46 +0200 Subject: [PATCH] Fix topology auth: pre-auth middleware reads gateway identity envelope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../StellaOps.Concelier.WebService/Program.cs | 59 +++++++++++++++++-- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/src/Concelier/StellaOps.Concelier.WebService/Program.cs b/src/Concelier/StellaOps.Concelier.WebService/Program.cs index 699493caf..8e12231ed 100644 --- a/src/Concelier/StellaOps.Concelier.WebService/Program.cs +++ b/src/Concelier/StellaOps.Concelier.WebService/Program.cs @@ -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( + 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() + .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() + .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() - ?.GetValue("STELLAOPS_IDENTITY_ENVELOPE_SIGNING_KEY") + var config = httpContext.RequestServices.GetService(); + var signingKey = config?.GetValue("Router:IdentityEnvelopeSigningKey") ?? Environment.GetEnvironmentVariable("STELLAOPS_IDENTITY_ENVELOPE_SIGNING_KEY"); if (string.IsNullOrWhiteSpace(signingKey))