Per-User Auth Broker
The auth broker is part of the server edition (//go:build server). It is opt-in per upstream — servers without an auth_broker block behave exactly as before. Brokering applies only to HTTP-family upstreams (http, sse, streamable-http); configuring it on a stdio upstream is rejected at config validation in this phase.
The auth broker lets the gateway acquire an upstream credential on behalf of the calling user instead of sharing a single static token. Each upstream server can declare how its credential is obtained via an auth_broker block on its server config (spec 074).
Modes
The auth_broker.mode field selects the credential-acquisition strategy:
| Mode | Description |
|---|---|
token_exchange | RFC 8693 OAuth 2.0 Token Exchange — swaps the caller's IdP token for an upstream-scoped token at the IdP token endpoint. |
entra_obo | Microsoft Entra On-Behalf-Of flow. |
oauth_connect | Path B — a per-user authorization-code + PKCE connect flow against an upstream authorization server that does not support token exchange. The user is redirected to the upstream's consent screen once; the resulting per-user credential is persisted encrypted and refreshed transparently. |
Configuration
The block lives under a server entry in the config file:
{
"mcpServers": [
{
"name": "github-enterprise",
"url": "https://ghe.example.com/mcp",
"protocol": "streamable-http",
"auth_broker": {
"mode": "oauth_connect",
"authorization_endpoint": "https://ghe.example.com/login/oauth/authorize",
"token_endpoint": "https://ghe.example.com/login/oauth/access_token",
"client_id": "Iv1.0123456789abcdef",
"client_secret": "GHE-secret",
"scopes": ["repo", "read:user"],
"resource": "https://ghe.example.com/mcp"
}
}
]
}
Fields
| Key | Required | Description |
|---|---|---|
mode | yes | One of token_exchange, entra_obo, oauth_connect. |
token_endpoint | yes | IdP/upstream token endpoint used to mint (and refresh) the upstream credential. |
authorization_endpoint | only for oauth_connect | Upstream authorization-server authorize URL the user is redirected to for consent. Required when mode is oauth_connect; ignored by token_exchange and entra_obo. |
resource | no | RFC 8707 audience the resulting token is scoped to. |
scopes | no | Scopes requested for the upstream credential. |
client_id | no¹ | Identifies the gateway to the token/authorization endpoint. |
client_secret | no | Authenticates a confidential client. A public client may omit it — PKCE still protects the oauth_connect code exchange. |
header | no | Outbound header the resolved credential is injected into (default Authorization). |
header_format | no | Value template; {token} is replaced with the resolved credential (default Bearer {token}). |
¹ client_id is required at runtime for the oauth_connect flow (the connector rejects an empty client ID); it is validated when the connect flow is assembled.
authorization_endpoint is mandatory for oauth_connectConfig validation fails with auth_broker.authorization_endpoint is required for mode "oauth_connect" if the key is missing while mode is oauth_connect. The other two modes never read it.
The oauth_connect flow (Path B)
- The gateway builds an authorize URL from
authorization_endpointwith a per-user opaquestateand a PKCES256challenge, and redirects the user there. - On the upstream's callback,
stateis validated as a known, unexpired, single-use pending flow (10-minute TTL) bound to the initiating user — confused-deputy / replay hardening. - The authorization code is exchanged at
token_endpointusing the bound PKCE verifier; the resulting credential is stored encrypted, per user, taggedObtainedVia=connect_flow. - Tokens are refreshed transparently from the stored refresh token; a non-rotating authorization server keeps its prior refresh token.
A denied consent (error=access_denied) clears the pending flow and stores nothing.
Credential resolution
On each proxied request the broker resolves the per-user credential to inject, in a strict per-user-only order. There is no shared or static fallback — a request that cannot produce a per-user credential fails rather than borrowing another identity:
- A valid cached per-user credential is injected directly; if it is within the near-expiry window it is refreshed first (re-minted for
token_exchange/entra_obo, or renewed from the stored refresh token foroauth_connect). - Otherwise, for
token_exchange/entra_obo, a credential is minted from the user's stored IdP subject token. - Otherwise, for
oauth_connectupstreams the user has not connected — or whose stored credential expired and could not be refreshed — the request fails with an actionable error carrying the connect URL, so the user is told to (re)connect rather than being silently denied. - Otherwise the request fails with "no per-user credential available".
Concurrent requests for the same (user, upstream) are coalesced (single-flight) so a burst does not trigger duplicate upstream token flows. A policy-decision hook is evaluated per call immediately before the credential is returned; no policy engine ships yet, so it permits every injection by default.
Header injection
The resolved per-user credential is injected into the configured outbound header (header, default Authorization) using the value template (header_format, default Bearer {token}), then the request is forwarded to the upstream.
Injection is a replacement, not a merge:
- Any header on the upstream config whose name matches
header(case-insensitively) is removed before the resolved credential is set, so a brokered upstream presents exactly one value for that header. - The inbound gateway/IdP token is never forwarded to the upstream. Brokering exists precisely so the upstream sees a credential minted for it, scoped to the calling user — not the token the user presented to the gateway.
Injection applies only to HTTP-family upstreams (http, sse, streamable-http). Brokering on a stdio upstream is rejected — at config validation, and again as a runtime guard at the injection boundary — with a clear "unsupported in this phase" message.
Per-(user, server) connection keying
A shared upstream that is brokered per-user must carry each user's own credential. Brokered upstream connections are therefore keyed by (user, server), never by server alone: one user's connection (and the credential injected on it) is never reused for another user. The server-component of the key reuses the same name + URL scheme as the credential store, so a connection and its cached credential stay in lockstep.
See also
- OAuth Authentication — upstream OAuth for the personal edition.
- Server multi-user authentication is covered in the project
CLAUDE.md(Spec 024).