# Authentication overview

The Zuplo MCP Gateway sits in the middle of two independent OAuth relationships.
MCP clients connect to the gateway and authenticate against it. The gateway, in
turn, connects to each upstream MCP server and authenticates against it on the
user's behalf. Both halves follow the same spec — the
[MCP authorization model](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization)
at revision `2025-11-25` — but they're configured separately and use different
policies.

This page explains both layers, the standards involved, and the moving parts
(sessions, scopes, token lifetimes) you'll see throughout the gateway. Per-IdP
setup lives in the dedicated guides:

- [Configuring Auth0](./configuring-auth0.mdx) — how-to
- [Configuring Okta (generic OIDC)](./configuring-okta.mdx) — how-to
- [Connect a gateway to an upstream OAuth provider](../how-to/connect-upstream-oauth.mdx)
  — how-to
- [Per-user OAuth to upstream MCP servers](./upstream-oauth.mdx) — the upstream
  side, conceptually
- [Manual OAuth testing](./manual-oauth-testing.mdx) — how-to

## The two layers

Every authenticated MCP request involves two distinct OAuth surfaces.

### Downstream: gateway as OAuth server

When a client like Claude Desktop, Cursor, or Claude Code connects to a
`/mcp/{slug}` route on the gateway, the client is the OAuth client and the
gateway is both the **OAuth 2.1 Resource Server (RS)** and the **OAuth 2.1
Authorization Server (AS)**.

The gateway publishes everything an MCP client needs to discover and complete an
OAuth flow:

- An RFC 9728 Protected Resource Metadata document per route.
- An RFC 8414 Authorization Server Metadata document, both gateway-wide and per
  route.
- An RFC 7591 Dynamic Client Registration endpoint.
- An OAuth Client ID Metadata Document (CIMD) acceptor.
- `/oauth/authorize`, `/oauth/token`, `/oauth/revoke`, and `/oauth/callback`
  endpoints.

Browser identity is delegated to an OIDC identity provider you configure —
Auth0, Okta, or any OIDC discovery-compatible IdP. The IdP authenticates the
user; the gateway then issues its own bearer access token to the MCP client.
**The IdP's token never reaches the MCP client.**

### Upstream: gateway as OAuth client

When the gateway forwards a request to an upstream MCP server (Linear, Stripe,
Notion, GitHub, your internal service, and so on), the gateway is the OAuth
client and the upstream MCP server is the resource server. On behalf of each
user, the gateway runs through the upstream provider's OAuth discovery,
registers itself (preferring OIDC Client ID Metadata Documents, falling back to
RFC 7591 Dynamic Client Registration), redirects the user through the upstream
`/authorize`, captures the upstream tokens, and stores them encrypted at rest.

On subsequent MCP requests, the gateway resolves the stored upstream credential
per user, refreshes it if necessary, and injects it as an
`Authorization: Bearer ...` header when proxying to the upstream.

:::caution{title="Token passthrough is forbidden"}

The MCP authorization spec
[explicitly forbids](https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices)
forwarding an inbound bearer token to an upstream API. Inbound auth headers
don't leak to the upstream — the gateway always uses an independent upstream
credential. The gateway-issued token a client presents and the upstream token
the gateway forwards are never the same token.

:::

## Standards observed

The gateway implements the following standards in their MCP-mandated subsets.

| Standard                                                                                                                           | Purpose                                                                                                                                                                                                     |
| ---------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [OAuth 2.1 (draft)](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13)                                                | Core authorization framework.                                                                                                                                                                               |
| [RFC 7636 — PKCE](https://datatracker.ietf.org/doc/html/rfc7636)                                                                   | Required on every authorization code flow. `S256` is required when technically capable.                                                                                                                     |
| [RFC 8414 — Authorization Server Metadata](https://datatracker.ietf.org/doc/html/rfc8414)                                          | Published at `/.well-known/oauth-authorization-server[/{routePath}]`.                                                                                                                                       |
| [OpenID Connect Discovery 1.0](https://openid.net/specs/openid-connect-discovery-1_0.html)                                         | Accepted alongside RFC 8414 as authorization-server discovery (added in the `2025-11-25` MCP revision).                                                                                                     |
| [RFC 9728 — Protected Resource Metadata](https://datatracker.ietf.org/doc/html/rfc9728)                                            | Published at `/.well-known/oauth-protected-resource/{routePath}` per MCP route.                                                                                                                             |
| [RFC 7591 — Dynamic Client Registration](https://datatracker.ietf.org/doc/html/rfc7591)                                            | Accepted at `/oauth/register`.                                                                                                                                                                              |
| [OAuth Client ID Metadata Documents (CIMD)](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-client-id-metadata-document-00) | Recommended client identification path per the `2025-11-25` MCP revision. The gateway advertises `client_id_metadata_document_supported: true` and accepts URLs as `client_id` values when CIMD is enabled. |
| [RFC 8707 — Resource Indicators](https://datatracker.ietf.org/doc/html/rfc8707)                                                    | MCP clients **MUST** include the `resource` parameter on every authorization and token request. The gateway validates that incoming bearer tokens were minted for the route's canonical resource URI.       |
| [RFC 6750 — Bearer tokens](https://datatracker.ietf.org/doc/html/rfc6750)                                                          | `Authorization: Bearer ...` only, header position only — tokens in query strings are rejected.                                                                                                              |
| [RFC 7009 — Token Revocation](https://datatracker.ietf.org/doc/html/rfc7009)                                                       | Published at `/oauth/revoke`.                                                                                                                                                                               |

CIMD is the recommended client identification path going forward; DCR is
retained for backwards compatibility with older MCP clients. Both work against
the same `/oauth/register` and AS metadata surface — clients that support either
are accommodated.

## Downstream flow

The downstream OAuth flow follows the spec's authorization-code grant with PKCE
plus the MCP `resource` parameter binding.

<Diagram height="h-72">
  <DiagramNode id="client">MCP Client</DiagramNode>
  <DiagramGroup id="gateway" label="Zuplo Gateway">
    <DiagramNode id="endpoints" variant="zuplo">
      OAuth endpoints
    </DiagramNode>
    <DiagramNode id="route" variant="zuplo">
      MCP route
    </DiagramNode>
  </DiagramGroup>
  <DiagramNode id="idp">Identity Provider</DiagramNode>
  <DiagramEdge from="client" to="endpoints" label="OAuth flow" />
  <DiagramEdge from="endpoints" to="idp" label="Browser login" />
  <DiagramEdge from="client" to="route" label="MCP request" />
</Diagram>

The flow is the standard MCP authorization handshake. The
[`McpGatewayPlugin`](../code-config/overview.mdx) registers the
`/.well-known/...` and `/oauth/...` endpoints automatically.

## Upstream flow

The first request to a route whose upstream needs OAuth produces a
**connect-required** error. The MCP client is expected to surface the returned
URL to the user; the user completes upstream OAuth in a browser; the next MCP
request succeeds.

<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 route
    </DiagramNode>
  </DiagramGroup>
  <DiagramNode id="provider">Upstream Provider</DiagramNode>
  <DiagramNode id="upstream">Upstream MCP server</DiagramNode>
  <DiagramEdge from="client" to="route" label="MCP request" />
  <DiagramEdge from="connect" to="provider" label="Upstream OAuth" />
  <DiagramEdge from="route" to="upstream" label="Proxied request" />
</Diagram>

The `connect-required` JSON-RPC error wraps an MCP
[`UrlElicitationRequiredError`](https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation),
so clients that implement the URL-elicitation extension open the URL in a
browser automatically. Older clients surface the URL as text for the user to
open manually.

When the gateway has a stored upstream connection for the user, no
connect-required error is returned — the proxy forwards transparently.

## Why two OAuth policies

The gateway ships two policies that protect the downstream side of a route. You
pick one per project.

| Policy                    | Use when                                                                                                                                              |
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mcp-auth0-oauth-inbound` | Auth0 is the IdP. Configure with `auth0Domain`, `clientId`, `clientSecret` and the wrapper derives the OIDC URLs for you.                             |
| `mcp-oauth-inbound`       | Any other OIDC IdP — Okta, Microsoft Entra ID, Keycloak, Ory Hydra, custom. Provide `oidc.issuer`, `oidc.jwksUrl`, and `browserLogin.url` explicitly. |

The two are functionally equivalent — `mcp-auth0-oauth-inbound` is a thin
wrapper around `mcp-oauth-inbound` that fills in the Auth0-flavored URLs. You
configure exactly one per project. The gateway rejects projects that declare
more than one MCP OAuth policy.

The upstream side uses a separate policy, `mcp-token-exchange-inbound`, one per
upstream MCP route. The downstream OAuth policy and the upstream token-exchange
policy are usually paired on the same route.

## Sessions, scopes, and TTLs

The defaults below affect user-visible behavior and come up often in
configuration.

### Browser session

After the user completes browser login, the gateway sets a `zuplo_mcp_session`
cookie. The cookie persists for **8 hours** by default
(`browserLogin.sessionTtlSeconds`). During that window, the user doesn't need to
re-authenticate against the IdP for additional OAuth grants — the consent page
renders immediately.

### Gateway-issued tokens

The gateway-issued bearer access token defaults to **15 minutes** of lifetime
(`gateway.accessTokenTtlSeconds = 900`). MCP clients refresh as needed.

Refresh tokens default to roughly **10 years**
(`gateway.refreshTokenTtlSeconds`). This is intentional: the gateway is not the
system of record for the user's session — the upstream IdP is. Imposing a
shorter refresh-token lifetime than the IdP's own session policy forces the user
back through browser login when the IdP would still accept a silent renewal.
Customers who want a tighter ceiling can override the default in policy options.

Refresh tokens rotate on every use, and presenting a previously rotated refresh
token revokes the entire grant (with a short grace window to handle concurrent
refreshes).

### Gateway scope

There is exactly one downstream OAuth scope today: `mcp:tools`. The gateway
issues every access token with this scope, and the PRM advertises it as the only
entry in `scopes_supported`. Future capability scopes will appear alongside
`mcp:tools` rather than replacing it.

Upstream OAuth scopes are independent — they're whatever the upstream provider
requires, configured per upstream on `mcp-token-exchange-inbound`.

### Authentication binding

Every gateway-issued access token is bound to:

- The **canonical resource URI** of the MCP route the user authorized for,
  derived from the request origin and the route path.
- The **`operationId`** of the route, set in `routes.oas.json`.

The gateway rejects a token presented at a different route or a different
canonical resource. A token issued for `/mcp/linear-v1` cannot be reused on
`/mcp/stripe-v1`.

The canonical resource URI is constructed from the incoming request origin. If
you front the gateway with a custom domain or a proxy, the gateway derives its
origin from the `Host` or `X-Forwarded-Host` header. A misconfigured proxy that
strips or overwrites these headers makes the gateway advertise the wrong issuer
in AS metadata. See [Troubleshooting](../troubleshooting.mdx) for symptoms.

## Custom domain caveat

The gateway's issuer URL — the value that appears as `issuer` in AS metadata and
as the authority of all generated endpoint URLs — is derived from the incoming
request's origin. The gateway honors the `Host` and `X-Forwarded-Host` headers
in that order.

When you put the gateway behind a custom domain (`gateway.example.com`), ensure
your fronting proxy or CDN forwards the original `Host` (or sets
`X-Forwarded-Host`) so the issuer in AS metadata matches the URL the MCP client
connected to. A mismatch makes OAuth clients reject the gateway's metadata.

## Endpoints reference

The gateway exposes the following authorization endpoints automatically once an
MCP OAuth policy is configured. See the [reference](../reference.mdx) for the
full URL catalog.

| Path                                                  | Method    | Purpose                                                                        |
| ----------------------------------------------------- | --------- | ------------------------------------------------------------------------------ |
| `/.well-known/oauth-authorization-server`             | GET       | RFC 8414 AS metadata (gateway-wide).                                           |
| `/.well-known/oauth-authorization-server/{routePath}` | GET       | RFC 8414 AS metadata (per route, rebinds `issuer`).                            |
| `/.well-known/oauth-protected-resource/{routePath}`   | GET       | RFC 9728 PRM (per route).                                                      |
| `/oauth/register`                                     | POST      | RFC 7591 Dynamic Client Registration.                                          |
| `/oauth/authorize`                                    | GET       | Gateway-wide authorize endpoint. Requires the `resource` parameter.            |
| `/oauth/authorize/{routePath}`                        | GET       | Per-route authorize endpoint.                                                  |
| `/oauth/callback`                                     | GET       | Browser-login callback from the IdP.                                           |
| `/oauth/setup`                                        | GET, POST | Consent and multi-upstream connect page.                                       |
| `/oauth/token`                                        | POST      | Token endpoint. Accepts `authorization_code` and `refresh_token` grants.       |
| `/oauth/revoke`                                       | POST      | RFC 7009 revocation.                                                           |
| `/.well-known/oauth-client/{connection}`              | GET       | OIDC Client ID Metadata Document for the upstream OAuth client (per upstream). |
| `/auth/connections/{connection}/connect`              | GET       | Start the upstream OAuth flow.                                                 |
| `/auth/connections/{connection}/callback`             | GET       | Upstream OAuth callback.                                                       |

The well-known metadata endpoints serve CORS-permissive responses
(`Access-Control-Allow-Origin: *`) because browser-resident MCP clients fetch
them cross-origin. The token, register, revoke, callback, setup, and connect
endpoints reject ambient credentials.

## Related

- [Configuring Auth0](./configuring-auth0.mdx) — Auth0 dashboard setup and
  policy configuration.
- [Configuring Okta](./configuring-okta.mdx) — any OIDC IdP through the generic
  `mcp-oauth-inbound` policy, using Okta as the worked example.
- [Per-user OAuth to upstream MCP servers](./upstream-oauth.mdx) — the upstream
  side, conceptually: discovery, client registration modes, per-user storage,
  refresh, and reconsent.
- [Connect a gateway to an upstream OAuth provider](../how-to/connect-upstream-oauth.mdx)
  — how to attach the `mcp-token-exchange-inbound` policy.
- [Manual OAuth testing](./manual-oauth-testing.mdx) — verify the gateway's
  OAuth surface end to end with `curl` and `openssl`.
- [Reference](../reference.mdx) — full URL catalog, default TTLs, and OAuth
  metadata extensions.
