Skip to content

feat(vmcp): inject user identity as HTTP headers into backend requests#5291

Open
fkztw wants to merge 1 commit into
stacklok:mainfrom
fkztw:feat/vmcp-inject-user-identity-headers
Open

feat(vmcp): inject user identity as HTTP headers into backend requests#5291
fkztw wants to merge 1 commit into
stacklok:mainfrom
fkztw:feat/vmcp-inject-user-identity-headers

Conversation

@fkztw
Copy link
Copy Markdown

@fkztw fkztw commented May 15, 2026

Summary

When vmcp forwards tool calls to backend MCP servers, the authenticated user's identity is now injected as HTTP request headers:

  • X-User-Sub: the sub claim from the authenticated token (set when a subject is present)
  • X-User-Email: the email claim (set only when non-empty)
  • X-User-Name: the name claim (set only when non-empty)

Motivation

When vmcp acts as an aggregating gateway, it validates the user's Bearer token via the configured incoming authentication strategy (OIDC, anonymous, or an embedded auth server). Backend MCP servers receive the forwarded request but currently have no information about which user initiated the call — only that vmcp accepted the request.

This makes it difficult for backends to:

  • Enforce per-user authorization at the application layer
  • Apply user-specific filtering (e.g. row-level access in a database)
  • Emit audit logs that attribute actions to a user

Injecting identity claims as request headers is a common pattern in API gateway architectures — see nginx auth_request propagation, Envoy ext_authz response headers, Google IAP X-Goog-Authenticated-User-Email, and AWS API Gateway request-context identity fields.

Implementation

A new claimInjectionRoundTripper is added to the per-backend transport chain in createMCPClient(), placed after the existing identityRoundTripper:

auth → identity context propagation → claim header injection → (size limit) → base transport

The tripper reads the *auth.Identity already attached at client-creation time and sets headers for non-empty claim values. When no identity is configured (e.g. anonymous mode without a populated identity), it is a no-op and the original request is forwarded unchanged.

The forwarded request is cloned before mutation; the caller-supplied request and its headers are not modified.

Type of change

  • New feature

Test plan

  • Unit tests (task test)
  • Linting (task lint-fix)
  • Manual testing (describe below)

Four new tests cover claimInjectionRoundTripper in roundtripper_test.go, following the same patterns as the existing identityRoundTripper tests:

  • All three fields injected when present
  • Email/name omitted from headers when empty
  • Empty subject does not inject X-User-Sub
  • Original request is cloned (not mutated)

Manual verification with a backend stub confirmed the expected headers reach the downstream service.

API Compatibility

  • This PR does not break the v1beta1 API.

Changes

File Change
pkg/vmcp/session/internal/backend/mcp_session.go Add claimInjectionRoundTripper and wire it into createMCPClient() transport chain
pkg/vmcp/session/internal/backend/roundtripper_test.go Add 4 tests for claimInjectionRoundTripper

Does this introduce a user-facing change?

Yes. Backend MCP servers connected via vmcp will now receive X-User-Sub, X-User-Email, and X-User-Name HTTP headers containing the authenticated user's identity claims. Servers that do not read these headers are unaffected.

When vmcp forwards tool calls to backend MCP servers, the authenticated
user's identity (sub, email, name) is now injected as HTTP request headers:

  X-User-Sub:   the sub claim from the authenticated token
  X-User-Email: the email claim (when present)
  X-User-Name:  the name claim (when present)

This allows backend MCP servers to identify the calling user without
needing to implement their own OAuth token introspection. Servers can
simply read these headers, which are set by the vmcp gateway after it
validates the Bearer token.

The injection is implemented as claimInjectionRoundTripper, added to the
transport chain in createMCPClient() after the existing identityRoundTripper.
When no identity is present in context (e.g. anonymous mode), no headers
are injected — the tripper is a no-op.

Signed-off-by: Frank Zheng <frank@tagtoo.com>
@fkztw fkztw force-pushed the feat/vmcp-inject-user-identity-headers branch from 0f894e9 to 333f308 Compare May 15, 2026 09:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant