ADR-0041: End-User Identity PropagationΒΆ
- Status: Accepted
- Date: 2026-02-17
- Deciders: Mihai Criveti
ContextΒΆ
MCP Gateway authenticates inbound requests and extracts user identity (email, teams, roles, admin status) but does not forward any of this identity information to downstream MCP servers when proxying. This means:
- Upstream MCP servers never receive the calling user's identity, making per-user authorization and auditing impossible at the upstream level.
- Plugins only see user identity when explicitly opted in via
include_user_info=True, and even then only limited fields. - Audit trails lack authentication method and delegation context, making compliance reporting incomplete.
- On-behalf-of flows (RFC 8693 token exchange) are not supported, preventing delegated access patterns common in enterprise environments.
This is a blocking requirement for multi-tenant deployments where upstream services need to enforce their own access control based on the original caller's identity.
DecisionΒΆ
Implement a comprehensive identity propagation system with the following components:
1. UserContext ModelΒΆ
A structured UserContext Pydantic model captures the full authenticated identity:
class UserContext(BaseModel):
user_id: str
email: Optional[str] = None
full_name: Optional[str] = None
is_admin: bool = False
groups: list[str] = Field(default_factory=list)
roles: list[str] = Field(default_factory=list)
team_id: Optional[str] = None
teams: Optional[list[str]] = None
department: Optional[str] = None
attributes: dict[str, Any] = Field(default_factory=dict)
auth_method: Optional[str] = None
authenticated_at: Optional[datetime] = None
service_account: Optional[str] = None
delegation_chain: list[str] = Field(default_factory=list)
This model is populated unconditionally for all authenticated requests (removing the include_user_info gate) and stored on GlobalContext.user_context.
2. Propagation MechanismsΒΆ
Identity is forwarded to upstream servers through two complementary mechanisms:
| Mechanism | Transport | Use Case |
|---|---|---|
| HTTP headers | X-Forwarded-User-* | REST proxying, health checks, tool invocation over HTTP |
MCP _meta field | JSON in MCP protocol | Native MCP tool calls, resource reads |
Operators choose via IDENTITY_PROPAGATION_MODE: headers, meta, or both (default).
3. Feature Flag and Per-Gateway OverrideΒΆ
- Global toggle:
IDENTITY_PROPAGATION_ENABLED=false(off by default, zero behavioral change for existing deployments) - Per-gateway override: Each gateway registration can set its own
identity_propagationJSON config, overriding mode, header prefix, signing, and allowed attributes.
4. Sensitive Attribute FilteringΒΆ
A configurable list (IDENTITY_SENSITIVE_ATTRIBUTES) strips internal-only fields (password hashes, internal IDs) before propagation. The UserContext is copied β the original is never mutated.
5. HMAC Claim SigningΒΆ
Optional HMAC-SHA256 signing (IDENTITY_SIGN_CLAIMS=true) allows upstream servers to verify that identity claims originated from the gateway and were not tampered with.
6. Audit Trail EnhancementΒΆ
Three new fields on AuditTrail records:
auth_method: How the user authenticated (bearer, api_key, basic, sso, proxy)acting_as: Service account identity for delegation scenariosdelegation_chain: Full chain of delegated identities
7. RFC 8693 Token ExchangeΒΆ
An OAuthManager.token_exchange() method supports on-behalf-of flows for gateways that require a user-scoped access token rather than client credentials.
ConsequencesΒΆ
PositiveΒΆ
- Upstream MCP servers can make per-user authorization decisions without trusting the gateway blindly
- Audit trails are complete with authentication method and delegation context
- Plugins always see identity β no more opt-in gate that most deployments forget to enable
- Per-gateway configuration allows gradual rollout and mixed-trust topologies
- Zero breaking changes β feature is off by default, existing
GlobalContext.userdict is preserved for backward compatibility - Claim signing provides cryptographic verification for zero-trust upstream environments
NegativeΒΆ
- Additional HTTP headers on every proxied request (minimal overhead β 6-10 headers, ~500 bytes)
- Per-gateway config adds complexity to gateway registration schema
- HMAC signing adds a small computational cost per request when enabled
Risks / MitigationsΒΆ
- Header injection: Upstream servers must trust that identity headers come from the gateway, not from clients. Mitigated by HMAC signing and by ensuring the gateway strips any client-provided
X-Forwarded-User-*headers before adding its own. - Sensitive data leakage: Mitigated by the configurable
IDENTITY_SENSITIVE_ATTRIBUTESfilter that strips internal fields before propagation. - Session isolation: Different users must get isolated upstream sessions. Mitigated by including identity headers in the session affinity key, ensuring different users get separate upstream connections.
Alternatives ConsideredΒΆ
| Option | Why Not |
|---|---|
| Mutual TLS with client certs per user | Too complex for most deployments; requires per-user certificate management |
| OAuth token forwarding (pass-through) | Exposes the gateway's internal tokens to upstream servers; violates least-privilege |
| Custom protocol-level identity field | Non-standard; HTTP headers and MCP _meta are the natural extension points |
| Always-on propagation (no feature flag) | Would change behavior for all existing deployments; feature flag allows opt-in |
RelatedΒΆ
- Feature: Identity Propagation Guide
- Configuration: Configuration Reference
- Architecture: Multi-tenancy
- Issue: #1436