# Per-user OAuth to upstream MCP servers

The gateway sits between an MCP client and the upstream MCP servers a team
relies on. The inbound OAuth surface is the one MCP clients connect to; the
outbound surface is where the gateway authenticates to each upstream on the
user's behalf. This page is about the outbound surface — the one the
`mcp-token-exchange-inbound` policy controls.

For the policy steps, options reference, and worked examples, see
[Connect a gateway to an upstream OAuth provider](../how-to/connect-upstream-oauth.mdx).

## Why the gateway acts as an OAuth client

Modern MCP servers — Linear, Notion, Stripe, GitHub, Grafana Cloud, and many
others — are OAuth-protected resources. They expect a `Bearer` token that
represents a specific user (or service identity) granted by their own OAuth
authorization server.

When an MCP client connects to a Zuplo MCP Gateway route, it presents the
gateway's bearer token. That token authenticates the user to the gateway but
isn't valid against the upstream. The spec
[explicitly forbids](https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices)
forwarding the inbound token to an upstream, so the gateway must mint an
independent upstream credential and attach it to the upstream request.

The gateway does that by acting as a standard OAuth client to each upstream —
discovering the upstream's authorization server, registering itself as a client,
redirecting the user through the upstream's authorization flow, capturing the
resulting tokens, and storing them encrypted at rest. On subsequent requests,
the gateway resolves the stored credential, refreshes it if necessary, and
applies it to the upstream request.

## The two auth modes

`authMode` is the central knob. It decides who owns the upstream credential.

### user-oauth

Per-user is the default and the right choice for most upstreams. Each user has
their own per-upstream OAuth connection. The first time a user hits the route,
the gateway returns a connect-required error; the user completes the upstream
provider's OAuth flow in a browser; the gateway stores the resulting tokens
encrypted, keyed by the user's subject ID. Subsequent requests from that user
are transparent.

This mode is what Linear, Notion, Stripe, GitHub, and most SaaS MCP servers use.
It preserves per-user attribution end to end — the upstream sees the specific
user making the call, and the gateway's analytics record the same subject ID
against every event.

### shared-oauth

Shared mode uses a single gateway-wide OAuth grant. There's no per-user connect
flow. An administrator completes a one-time connection through the upstream's
OAuth provider, and every authenticated user reuses that credential when calling
the upstream. If no shared connection exists yet, the gateway returns an
`admin_connect_required` error to let the client know an administrator action is
needed.

Shared mode is appropriate when the upstream uses a service account that
represents the organization rather than individual users, or when auditing
happens at the gateway level (per user) rather than at the upstream (where every
call looks like the same service account).

## Client registration

The gateway needs to identify itself to the upstream OAuth provider before it
can request tokens. The `clientRegistration` option controls how:

- **CIMD with DCR fallback (`{ "mode": "auto" }`)** — the default. The gateway
  publishes a per-upstream
  [OAuth Client ID Metadata Document](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-client-id-metadata-document-00)
  at `/.well-known/oauth-client/{connection}?authProfileId=...` and tells the
  upstream that URL is the client ID. If the upstream doesn't accept CIMD, the
  gateway falls back to
  [RFC 7591 Dynamic Client Registration](https://datatracker.ietf.org/doc/html/rfc7591).
  Auto mode requires nothing from the upstream provider beyond standard MCP
  authorization spec support and has no client secrets to rotate.
- **Manual** — the gateway uses a pre-registered `clientId` (and optional
  `clientSecret`) and authenticates to the upstream token endpoint with a
  configured method. Manual mode is the right choice when an organization
  manages OAuth client lifecycle centrally, the upstream provider requires an
  approved client, or one OAuth client should be shared across multiple routes.

Both modes are first-class. CIMD documents are accessible to the upstream
provider over HTTPS — the upstream fetches them as part of its OAuth
registration flow. The CIMD URL includes the `authProfileId` query parameter so
the gateway can scope client identity per `(upstream, authMode)` pair.

## How the gateway picks scopes

The gateway needs to know which OAuth scopes to request from the upstream. It
considers three sources in order:

1. **An explicit `scopes` array on the policy.** When set, the gateway uses
   exactly those values on every upstream authorization request.
2. **The `scope=` value from the upstream's most recent `WWW-Authenticate`
   challenge.** Used when no explicit scopes are configured.
3. **The `scopes_supported` array in the upstream's Protected Resource
   Metadata.** Used as the final fallback before falling through to no `scope`
   parameter at all.

Explicit scopes always win. Microsoft 365, Slack, PostHog, Stripe, and Grafana
Cloud are examples of upstreams that need explicit scopes — their PRM either
lists too many scopes or none at all, so deferring to discovery alone isn't
enough.

## What the user sees

The browser flow runs the first time a user hits an OAuth-protected upstream
they haven't connected, and again whenever the upstream revokes the gateway's
client. Modern MCP clients implement the URL-elicitation extension and open the
URL automatically. Older clients surface the URL as part of the JSON-RPC error
message; the user copies it into a browser.

<Diagram height="h-72">
  <DiagramNode id="client">MCP Client</DiagramNode>
  <DiagramGroup id="gateway" label="Zuplo Gateway">
    <DiagramNode id="connect" variant="zuplo">
      Upstream connect
    </DiagramNode>
    <DiagramNode id="route" variant="zuplo">
      /mcp/linear-v1
    </DiagramNode>
  </DiagramGroup>
  <DiagramNode id="oauth">Linear OAuth</DiagramNode>
  <DiagramNode id="upstream">Linear MCP</DiagramNode>
  <DiagramEdge from="client" to="route" label="MCP request" />
  <DiagramEdge from="connect" to="oauth" label="Upstream OAuth" />
  <DiagramEdge from="route" to="upstream" label="Proxied request" />
</Diagram>

Each MCP route proxies to exactly one upstream, so the consent page typically
shows one upstream to connect. The consent page is part of the gateway and
renders automatically whenever a user lands at `/oauth/setup` mid-flow.

## Connect-required states

When the gateway needs the user to act, it returns a JSON-RPC error with a
`state` field that distinguishes the three reasons.

| State                    | Meaning                                                                                                                  | Typical UI message                                                             |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ |
| `authenticating`         | First-time connection. User hasn't authorized the upstream yet.                                                          | "Connect to `{provider}` to continue."                                         |
| `reconsent_required`     | Existing connection but the upstream revoked the client or invalidated the refresh token. The user needs to reauthorize. | "`{provider}` authorization must be renewed."                                  |
| `admin_connect_required` | `authMode: shared-oauth` and no shared connection exists yet. Only an administrator can complete the flow.               | "An administrator must connect `{provider}` before this service is available." |

The full JSON-RPC error payload looks like:

```jsonc
{
  "jsonrpc": "2.0",
  "id": "1",
  "error": {
    "code": -32042,
    "message": "Connect Linear to continue.",
    "data": {
      "state": "authenticating",
      "upstreamServerId": "linear",
      "operationId": "linear-mcp-server",
      "authUrl": "https://gateway.example.com/auth/connections/linear/connect?browserTicket=eyJ...&operationId=linear-mcp-server",
      "nextAction": "redirect",
      "authProfileId": "linear:user-oauth",
    },
  },
}
```

The `-32042` error code is MCP's `URLElicitationRequiredError`. Clients that
support URL elicitation open `authUrl` directly; others render the message and
let the user open the URL manually.

## Refresh and 401 retry

The gateway transparently refreshes the upstream access token from the stored
refresh token. When the upstream returns a 401 mid-request — for example,
because the upstream's session-bound token expired — the gateway refreshes the
upstream credential and retries the upstream fetch once. If the refresh fails or
produces another connect-required state, the gateway returns the JSON-RPC
connect-required to the client and the user sees the reconsent flow.

Stored refresh tokens stay valid as long as the upstream provider honors them.
When an upstream's policy revokes a refresh token — for example, because the
user revoked the connection from the upstream's dashboard — the next request
surfaces `reconsent_required` and the user re-authorizes through the same
browser flow.

## Where the metadata URL comes from

By default, the gateway derives the upstream Protected Resource Metadata URL
from the route's `rewritePattern`:

```text
rewritePattern:                https://mcp.linear.app/mcp
default PRM URL:               https://mcp.linear.app/.well-known/oauth-protected-resource/mcp
```

When the upstream serves PRM at a non-default path (Linear's PRM lives at the
origin's root, not under `/mcp`), the policy's `protectedResourceMetadataUrl`
option overrides the default. The canonical source of truth is the
`resource_metadata=` parameter on the upstream's `WWW-Authenticate` challenge to
an unauthenticated request.

## Related

- [Connect a gateway to an upstream OAuth provider](../how-to/connect-upstream-oauth.mdx)
  — how to attach the policy, pick modes, and verify the connect flow.
- [Authentication overview](./overview.mdx) — the two-layer model and how
  inbound and outbound OAuth fit together.
- [Manual OAuth testing](./manual-oauth-testing.mdx) — verify the gateway's
  OAuth surface end to end with `curl` and `openssl`.
- [Compatibility dates](../code-config/compatibility-dates.mdx) — the
  `2026-03-01` requirement for upstream 401 retries and other MCP Gateway
  behaviors.
