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
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 Okta (generic OIDC)
- Per-user OAuth to upstream MCP servers
- Manual OAuth testing
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/callbackendpoints.
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.
Token passthrough is forbidden
The MCP authorization spec
explicitly forbids
forwarding an inbound bearer token to an upstream API. The gateway always strips
the inbound Authorization header before forwarding and 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) | Core authorization framework. |
| RFC 7636 — PKCE | Required on every authorization code flow. S256 is required when technically capable. |
| RFC 8414 — Authorization Server Metadata | Published at /.well-known/oauth-authorization-server[/{routePath}]. |
| OpenID Connect Discovery 1.0 | Accepted alongside RFC 8414 as authorization-server discovery (added in the 2025-11-25 MCP revision). |
| RFC 9728 — Protected Resource Metadata | Published at /.well-known/oauth-protected-resource/{routePath} per MCP route. |
| RFC 7591 — Dynamic Client Registration | Accepted at /oauth/register. |
| OAuth Client ID Metadata Documents (CIMD) | 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 | 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 | Authorization: Bearer ... only, header position only — tokens in query strings are rejected. |
| RFC 7009 — Token Revocation | 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.
Code
The flow is the standard MCP authorization handshake; the gateway plays the RS
role on every /mcp/{slug} route and the AS role on the /.well-known/... and
/oauth/... routes registered automatically by the
McpGatewayPlugin.
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.
Code
The connect-required JSON-RPC error wraps an MCP
UrlElicitationRequiredError,
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
A few defaults are worth remembering when reasoning about the gateway.
Browser session
After the user completes browser login, the gateway sets a __mcp_session
cookie that carries a signed session token. 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.
Both tokens are opaque random strings; only their SHA-256 hashes are stored. 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
operationIdof the route, set inroutes.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 for symptoms.
Endpoints reference
The gateway exposes the following authorization endpoints automatically once an MCP OAuth policy is configured. See the reference 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.
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.
Next steps
- Configuring Auth0 — set up the Auth0 wrapper policy
with
auth0Domain,clientId, andclientSecret. - Configuring Okta — set up any OIDC IdP through the
generic
mcp-oauth-inboundpolicy, using Okta as the worked example. - Per-user OAuth to upstream MCP servers — the upstream side: discovery, client registration modes, per-user storage, refresh, and reconsent.
- Manual OAuth testing — verify the gateway's
OAuth surface end to end with
curlandopenssl.