stacklok / stacklok/toolhive

feat(vmcp): composing per-user federated-IdP backends behind vmcp — linked-account model or per-backend IncomingAuth overrides

Open
#5,383 4 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

authentication authorization enhancement go
Dominant language
Go
Stars
2.2k
Forks
300
Avg merge
1d 15h
Merged PRs (30d)
184

Description

Summary

There is no supported pattern today for placing a backend whose upstream
accepts only user-owned OAuth tokens (GitLab, GitHub, etc.) inside a
VirtualMCPServer while preserving per-user identity at that upstream.
Two independent architectural constraints block it. This issue describes
the gap precisely, documents a confirmed working standalone workaround, and
proposes two approaches that would close it.


Concrete use case

Setup: A VirtualMCPServer fronts ~10 backends. Inbound auth is a
corporate Keycloak instance. Outbound auth for all other backends uses
outgoingAuth.source: discovered + per-backend MCPExternalAuthConfig
of type tokenExchange against a mcp-exchange-svc Keycloak client —
Keycloak mints backend-scoped JWTs from the inbound token. This works for
any backend whose upstream accepts a Keycloak-issued JWT.

Desired addition: an MCPRemoteProxy for the GitLab native MCP
endpoint

(/api/v4/mcp) behind the same vmcp gateway, with per-user identity at
GitLab (not a shared service account PAT).

Why token exchange cannot bridge this: GitLab's /oauth/token does
not implement RFC 8693 — it returns unsupported_grant_type on
urn:ietf:params:oauth:grant-type:token-exchange requests.
MCPExternalAuthConfig type: tokenExchange therefore cannot produce a
GitLab-issued token from a Keycloak JWT.

Standalone workaround (confirmed working): Run the GitLab proxy as a
standalone MCPRemoteProxy — no groupRef, no vmcp — with its own
externalAuthConfigRef → embeddedAuthServer federating to GitLab as the
sole upstream IdP. Clients do a one-time GitLab OAuth flow; the embedded
AS stores per-user refresh tokens and injects the upstream GitLab token
on each request (upstreamInject). This gives correct per-user identity
but produces two separate MCP endpoints instead of one unified gateway.


Gap 1 — IncomingAuth is gateway-wide; no per-backend override

VirtualMCPServerSpec.IncomingAuth is a single field that applies to
every client connecting to vmcp. BackendAuthConfig exists only in the
outgoing direction (vmcp → backends). There is no mechanism to say
"clients authenticating to reach the GitLab backend must go through
embeddedAuthServer; clients reaching all other backends authenticate
via Keycloak OIDC directly."

If IncomingAuth is switched to embeddedAuthServer (GitLab upstream),
Keycloak-issued tokens are no longer accepted at all, breaking every
other backend.


Gap 2 — upstreamProviders are alternates, not a chain

Multiple entries in embeddedAuthServer.upstreamProviders are treated as
alternate IdPs for the same user session (only the first is authoritative;
the operator emits a warning condition when multiple upstreams are
combined with authz policy). There is no way to say "authenticate the
client via Keycloak (inbound), and in parallel acquire and store a GitLab
token for this session to inject into the GitLab backend."

When a user authenticates through the Keycloak upstream, no GitLab token
is ever acquired. The upstreamInject pattern only works in the
standalone case because the embedded AS's sole upstream is GitLab and
every session already carries a GitLab access token.


Proposed solutions

Option A — Linked-account model in embeddedAuthServer

Extend the embedded AS to support a linked-account flow: the user's
primary authentication goes through the configured inboundProvider
(e.g., Keycloak). For each declared linkedProvider, the embedded AS
separately initiates an OAuth authorization-code flow against the
external IdP (GitLab) and persists the per-user upstream access + refresh
token in its session store, associated with the primary sub.

On each proxied request, if the target backend is the one linked to
GitLab, the stored GitLab access token is injected (upstreamInject).
For all other backends the existing Keycloak tokenExchange outgoing
auth applies unchanged.

This is the higher-value path because it preserves a single unified MCP
endpoint and keeps inbound auth on Keycloak for all clients.

Option B — Per-backend IncomingAuth override on VirtualMCPServer

Add an optional incomingAuthOverride field at the BackendRef level in
VirtualMCPServerSpec. When set, vmcp uses the specified
MCPExternalAuthConfig to validate tokens on requests routed to that
backend, bypassing the gateway-wide IncomingAuth. Clients must hold a
valid token for the backend-specific config; the gateway routes by
matching incoming bearer token issuer against registered configs.

This is the more targeted change and may be easier to implement, though
it still does not acquire GitLab tokens automatically — clients would need
to obtain them independently and present them in a backend-specific
header or scope hint, which degrades UX.


Related issues

  • #5218 — ID-JAG/XAA: an alternative bridge if GitLab ever supports
    identity-assertion grants. Not available on self-managed instances
    today, and depends on the upstream IdP implementing the draft spec.
  • #5194 — embeddedAuthServer RFC 8693 token exchange: a superset of
    this request for the agentic case; the linked-account model in Option A
    is the user-interactive analog.

Environment

  • ToolHive operator: v0.27.0 (Kubernetes, self-managed deployment)
  • Keycloak 26.3 (inbound IdP)
  • GitLab self-managed (upstream backend, /api/v4/mcp)
  • Confirmed by Stacklok/Yolanda on Discord (2026-05-19) that neither gap
    is a misconfiguration — both are current architectural limits with no
    workaround inside vmcp.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reviewing VirtualMCPServerSpec.IncomingAuth, BackendRef, embeddedAuthServer, and MCPExternalAuthConfig, then compare the linked-account and per-backend override proposals. A maintainer must select and define the architecture before implementation; done should preserve Keycloak access for existing backends while providing per-user GitLab identity through the unified vmcp endpoint.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
authentication, backend-api-design
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.