# Gateway · Identity Header Policy for Router Dispatch ## Status - **Implemented** in Sprint 8100.0011.0002. - Middleware: `src/Gateway/StellaOps.Gateway.WebService/Middleware/IdentityHeaderPolicyMiddleware.cs` - Last updated: 2025-12-24 (UTC). ## Why This Exists The Gateway is the single HTTP ingress point and routes requests to internal microservices over Router transports. Many services (and legacy components) still rely on **header-based identity context** (tenant/scopes/actor) rather than (or in addition to) `HttpContext.User` claims. This creates a hard security requirement: - **Clients must never be able to inject/override “roles/scopes” headers** that the downstream service trusts. - The Gateway must derive the effective identity from the validated JWT/JWK token (or explicit anonymous identity) and **overwrite** downstream identity headers accordingly. ## Implementation The `IdentityHeaderPolicyMiddleware` (introduced in Sprint 8100.0011.0002) replaces the legacy middleware: - ~~`src/Gateway/StellaOps.Gateway.WebService/Middleware/ClaimsPropagationMiddleware.cs`~~ (retired) - ~~`src/Gateway/StellaOps.Gateway.WebService/Middleware/TenantMiddleware.cs`~~ (retired) ### Resolved issues 1) **Spoofing risk:** ✅ Fixed. Middleware uses "strip-and-overwrite" semantics—reserved headers are stripped before claims are extracted and downstream headers are written. 2) **Claim type mismatch:** ✅ Fixed. Middleware uses `StellaOpsClaimTypes.Tenant` (`stellaops:tenant`) with fallback to legacy `tid` claim. 3) **Scope claim mismatch:** ✅ Fixed. Middleware extracts scopes from both `scp` (individual claims) and `scope` (space-separated) claims. 4) **Docs alignment:** ✅ Reconciled in this sprint. ## Policy Goals - **No client-controlled identity headers:** the Gateway rejects or strips identity headers coming from external clients. - **Gateway-controlled propagation:** the Gateway sets downstream identity headers based on validated token claims or a defined anonymous identity. - **Compatibility bridge:** support both `X-Stella-*` and `X-StellaOps-*` header naming during migration. - **Determinism:** header values are canonicalized (whitespace, ordering) and do not vary across equivalent requests. ## Reserved Headers (Draft) The following headers are considered **reserved identity context** and must not be trusted from external clients: - Tenant / project: - `X-StellaOps-Tenant`, `X-Stella-Tenant` - `X-StellaOps-Project`, `X-Stella-Project` - Scopes / roles: - `X-StellaOps-Scopes`, `X-Stella-Scopes` - Actor / subject (if used for auditing): - `X-StellaOps-Actor`, `X-Stella-Actor` - Token proof / confirmation (if propagated): - `cnf`, `cnf.jkt` **Internal/legacy pass-through keys to also treat as reserved:** - `sub`, `scope`, `scp`, `tid` (legacy), `stellaops:tenant` (if ever used as a header key) ## Overwrite Rules (Draft) For non-system paths (i.e., requests that will be routed to microservices): 1) **Strip** all reserved identity headers from the incoming request. 2) **Compute** effective identity from the authenticated principal: - `sub` from JWT `sub` (`StellaOpsClaimTypes.Subject`) - `tenant` from `stellaops:tenant` (`StellaOpsClaimTypes.Tenant`) - `project` from `stellaops:project` (`StellaOpsClaimTypes.Project`) when present - `scopes` from: - `scp` claims (`StellaOpsClaimTypes.ScopeItem`) if present, else - split `scope` (`StellaOpsClaimTypes.Scope`) by spaces 3) **Write** downstream headers (compat mode): - Tenant: - `X-StellaOps-Tenant: ` - `X-Stella-Tenant: ` (optional during migration) - Project (optional): - `X-StellaOps-Project: ` - `X-Stella-Project: ` (optional during migration) - Scopes: - `X-StellaOps-Scopes: ` - `X-Stella-Scopes: ` (optional during migration) - Actor: - `X-StellaOps-Actor: ` (unless another canonical actor claim is defined) ### Anonymous mode If `Gateway:Auth:AllowAnonymous=true` and the request is unauthenticated: - Set an explicit anonymous identity so downstream services never interpret “missing header” as privileged: - `X-StellaOps-Actor: anonymous` - `X-StellaOps-Scopes: ` (empty) or `anonymous` (choose one and document) - Tenant behavior must be explicitly defined: - either reject routed requests without tenant context, or - require a tenant header even in anonymous mode and treat it as *untrusted input* that only selects a tenancy partition (not privileges). ## Scope Override Header (Offline/Pre-prod) Some legacy flows allow setting scopes via headers (for offline kits or pre-prod bundles). Draft enforcement: - Default: **forbid** client-provided scope headers (`X-StellaOps-Scopes`, `X-Stella-Scopes`) and return 403 (deterministic error code). - Optional controlled override: allow only when `Gateway:Auth:AllowScopeHeader=true`, and only for explicitly allowed environments (offline/pre-prod). - Even when allowed, the override must not silently escalate a request beyond what the token allows unless the request is explicitly unauthenticated and the environment is configured for offline operation. ## Implementation Details ### Middleware Registration The middleware is registered in `Program.cs` after authentication: ```csharp app.UseAuthentication(); app.UseMiddleware(); app.UseMiddleware(); ``` ### Configuration Options are configured via `GatewayOptions.Auth`: ```yaml Gateway: Auth: EnableLegacyHeaders: true # Write X-Stella-* in addition to X-StellaOps-* AllowScopeHeader: false # Forbid client scope headers (default) ``` ### HttpContext.Items Keys The middleware stores normalized identity in `HttpContext.Items` using `GatewayContextKeys`: - `Gateway.TenantId` — extracted tenant identifier - `Gateway.ProjectId` — extracted project identifier (optional) - `Gateway.Actor` — subject/actor from claims or "anonymous" - `Gateway.Scopes` — `HashSet` of scopes - `Gateway.IsAnonymous` — `bool` indicating anonymous request - `Gateway.DpopThumbprint` — JKT from cnf claim (if present) - `Gateway.CnfJson` — raw cnf claim JSON (if present) ### Tests Located in `src/Gateway/__Tests/StellaOps.Gateway.WebService.Tests/Middleware/IdentityHeaderPolicyMiddlewareTests.cs`: - ✅ Spoofed identity headers are stripped and overwritten - ✅ Claim type mapping uses `StellaOpsClaimTypes.*` correctly - ✅ Anonymous requests receive explicit `anonymous` identity - ✅ Legacy headers are written when `EnableLegacyHeaders=true` - ✅ Scopes are sorted deterministically ## Related Documents - Gateway architecture: `docs/modules/gateway/architecture.md` - Tenant auth contract (Web V): `docs/api/gateway/tenant-auth.md` - Router ASP.NET bridge: `docs/modules/router/aspnet-endpoint-bridge.md`