130 lines
6.8 KiB
Markdown
130 lines
6.8 KiB
Markdown
# 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: <tenant>`
|
|
- `X-Stella-Tenant: <tenant>` (optional during migration)
|
|
- Project (optional):
|
|
- `X-StellaOps-Project: <project>`
|
|
- `X-Stella-Project: <project>` (optional during migration)
|
|
- Scopes:
|
|
- `X-StellaOps-Scopes: <space-delimited scopes>`
|
|
- `X-Stella-Scopes: <space-delimited scopes>` (optional during migration)
|
|
- Actor:
|
|
- `X-StellaOps-Actor: <sub>` (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<SenderConstraintMiddleware>();
|
|
app.UseMiddleware<IdentityHeaderPolicyMiddleware>();
|
|
```
|
|
|
|
### 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<string>` 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`
|
|
|