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))