Trusted-Gateway Identity

Run the server behind an MCP-aware reverse proxy that authenticates users and injects a signed identity assertion (a JWT) on every upstream request. The server verifies that assertion against the proxy's JWKS and uses the asserted email as the per-request principal, giving true per-user isolation for proxy-fronted deployments.

Synced with v1.25.1 ·

When to Use It

Use trusted-gateway identity when you have an SSO gateway (Pomerium, oauth2-proxy, Cloudflare Access, Istio/Envoy, Traefik ForwardAuth, or similar) doing authentication and per-route authorization in front of the server, and you don't want the MCP server to also terminate OAuth (which would contend for the single MCP auth handshake).

With this mode enabled, the asserted identity (not a client-supplied value) decides which user's Google credential a request may use. The Google credential is keyed to the verified principal, not to the transport session, so each user gets true per-user isolation even behind a shared proxy.

Keep MCP_ENABLE_OAUTH21=false (the proxy owns the handshake). TRUST_GATEWAY_IDENTITY is mutually exclusive with MCP_ENABLE_OAUTH21=true.

Configuration

VariableDefaultNotes
TRUST_GATEWAY_IDENTITY requiredfalseEnable trusted-gateway identity mode
GATEWAY_IDENTITY_JWKS_URL required-Proxy JWKS endpoint used to verify the assertion. Must use HTTPS (HTTP is permitted only for loopback development endpoints).
GATEWAY_IDENTITY_HEADER x-pomerium-jwt-assertionHeader carrying the JWT (e.g. cf-access-jwt-assertion for Cloudflare Access)
GATEWAY_IDENTITY_ALGORITHMS ES256Comma-separated allowed signing algorithm(s). Must use asymmetric algorithms from a single family: ES (ES256, ES384, ES512), RS (RS256, RS384, RS512), PS (PS256, PS384, PS512), or EdDSA. Pinned to block alg-confusion and none attacks.
GATEWAY_IDENTITY_ISSUER -If set, the assertion's iss claim must match
GATEWAY_IDENTITY_AUDIENCE required-Assertion aud identifying this MCP deployment; always verified

The proxy must overwrite or remove any client-supplied identity header before forwarding its own assertion to the upstream. For Pomerium, set pass_identity_headers: true on the route.

How It Works

  1. Verify - the assertion JWT is validated against the proxy's JWKS (signature + exp + aud; optional iss), pinned to the configured algorithm(s). A missing or invalid assertion rejects the request immediately; gateway mode never falls back to bearer tokens or transport-session identity.
  2. Principal - the verified email claim becomes authenticated_user_email (authenticated_via=gateway_assertion) in request-scoped state only.
  3. No prompt, no spoofing - like OAuth 2.1 mode, the user_google_email tool parameter is hidden and auto-filled from the verified principal, so clients never ask for an email and a caller can't act on another account by passing one. Any caller-supplied user_google_email is silently dropped (cached pre-gateway schemas keep working), and a configured USER_GOOGLE_EMAIL default is ignored.
  4. Consent enforcement - the per-user Google consent (side flow) is initiated for the principal. The OAuth state records an explicit immutable principal binding, and at /oauth2callback the Google account actually consented must match it; a missing binding or mismatch is rejected and nothing is stored.

Credentials still use the normal per-user store (keyed by email); the asserted identity selects and locks which user's grant a request may use.

Provider Examples

Pomerium

Pomerium is the default target. Its identity header (x-pomerium-jwt-assertion) and algorithm (ES256) are the built-in defaults, so only the JWKS URL and audience need to be set:

TRUST_GATEWAY_IDENTITY=true
GATEWAY_IDENTITY_JWKS_URL=https://authenticate.example.com/.well-known/pomerium/jwks.json
GATEWAY_IDENTITY_AUDIENCE=workspace-mcp.example.com
# header/alg defaults already target Pomerium

Cloudflare Access

Cloudflare Access uses RS256 and injects the assertion in cf-access-jwt-assertion. The JWKS URL follows the pattern https://<team-name>.cloudflareaccess.com/cdn-cgi/access/certs:

TRUST_GATEWAY_IDENTITY=true
GATEWAY_IDENTITY_JWKS_URL=https://<team-name>.cloudflareaccess.com/cdn-cgi/access/certs
GATEWAY_IDENTITY_HEADER=cf-access-jwt-assertion
GATEWAY_IDENTITY_ALGORITHMS=RS256
GATEWAY_IDENTITY_AUDIENCE=<your-access-application-aud>

Other proxies

Any proxy that injects a JWKS-verifiable JWT identity header works. Set GATEWAY_IDENTITY_HEADER to the header name, GATEWAY_IDENTITY_ALGORITHMS to the proxy's signing algorithm, and point GATEWAY_IDENTITY_JWKS_URL at the proxy's key-discovery endpoint. oauth2-proxy, Istio/Envoy, and Traefik ForwardAuth all support this pattern.

Security Notes

  • The assertion is verified cryptographically - an unverified, expired, or wrong-algorithm token is rejected.
  • Pin GATEWAY_IDENTITY_ALGORITHMS to your proxy's actual algorithm. Only asymmetric algorithms are accepted (no HMAC), and all configured algorithms must belong to the same family (e.g. all ES or all RS).
  • GATEWAY_IDENTITY_AUDIENCE is mandatory so assertions minted for another application or route cannot be replayed here. Set GATEWAY_IDENTITY_ISSUER as well when your gateway provides a stable issuer.
  • GATEWAY_IDENTITY_JWKS_URL must use HTTPS. HTTP is permitted only for loopback addresses (localhost / 127.0.0.1) during development.
  • Configure the proxy to strip or overwrite incoming GATEWAY_IDENTITY_HEADER values from clients.
  • Ensure the backend is reachable only via the proxy (e.g. ClusterIP, no public port), so the assertion can't be supplied by an untrusted client.