Skip to main content

MCP + Upstream OAuth

Upstream OAuth applies to two scenarios:

  • Third-party hosted MCP servers (Notion, Linear, Google) that enforce their own OAuth — Pomerium handles the upstream auth flow so your users authenticate once and tool calls are proxied with valid tokens.
  • Self-hosted MCP servers that call upstream APIs under the hood — your server receives a valid upstream access token in the Authorization: Bearer header on every proxied request, so it never needs to implement OAuth itself.

In both cases, external MCP clients only hold a Pomerium-issued token (TE) and never see upstream credentials.

Pomerium uses a unified upstream OAuth pipeline that adapts based on what you configure. The MCP authorization spec defines a priority order for how clients identify themselves to upstream authorization servers:

  1. Pre-registered credentials — use when the upstream authorization server requires manual app registration and does not support automatic client registration. You supply client_id and client_secret (and optionally endpoints) in your route config.
  2. CIMD — the recommended automatic mechanism. Pomerium publishes its client metadata at an HTTPS URL that serves as its client_id. The upstream AS fetches metadata on demand.
  3. DCR — a fallback for authorization servers that support dynamic registration but not CIMD.

When no upstream_oauth2 config is present, Pomerium tries CIMD first, then falls back to DCR. When client_id and client_secret are provided, Pomerium uses them directly and skips automatic registration. When endpoints are also provided, Pomerium skips RFC 9728 discovery as well.

Architecture

MCP ServerPomeriumUpstream OAuthMCP ClientMCP ServerPomeriumUpstream OAuthMCP ClientUserAdds a server URLRegisters client, initiates authSign-in URLRedirect to sign-in URLSign-inRedirect to upstream OAuthAuthenticate with upstream OAuthReturn Internal Token (TI)Redirect to clientObtain External Token (TE)GET https://mcp-server Authorization: Bearer (TE)Refresh (TI) if necessaryProxy request to MCP Server, Bearer (TI)User

Static OAuth2

Use static OAuth2 when you have pre-registered OAuth credentials and know the provider's endpoints.

Configuration

runtime_flags:
mcp: true

routes:
- from: https://github.your-domain.com
to: http://github-mcp.int:8080/mcp
name: GitHub
mcp:
server:
upstream_oauth2:
client_id: xxxxxxxxxxxx
client_secret: yyyyyyyyy
scopes: ['read:user', 'user:email']
endpoint:
auth_url: 'https://github.com/login/oauth/authorize'
token_url: 'https://github.com/login/oauth/access_token'
# Optional: extra query parameters for the authorization URL
# authorization_url_params:
# access_type: 'offline'
# prompt: 'consent'
policy:
allow:
and:
- domain:
is: company.com
deny:
and:
- mcp_tool:
starts_with: 'admin_'
Experimental Feature

MCP support is currently an experimental feature only available in the main branch or Docker images built from main. To enable MCP functionality, you must set runtime_flags.mcp: true in your Pomerium configuration.

Step-by-step

1. Create an OAuth app with your upstream provider

Register an OAuth application with the upstream service. For GitHub:

  1. Go to Settings → Developer settings → OAuth Apps → New OAuth App
  2. Set the authorization callback URL to https://<your-route-from-domain>/.pomerium/mcp/client/oauth/callback — for example, if your route's from is https://github.your-domain.com, the callback URL is https://github.your-domain.com/.pomerium/mcp/client/oauth/callback
  3. Note the Client ID and Client Secret

For other providers, refer to their OAuth documentation. You need:

  • client_id and client_secret
  • auth_url and token_url endpoints
  • The appropriate scopes for your use-case
  • The redirect/callback URI set to https://<your-route-from-domain>/.pomerium/mcp/client/oauth/callback

2. Configure the route

Add the route configuration shown above. The key addition compared to a basic MCP server route is the upstream_oauth2 block under mcp.server.

See the MCP Full Reference for all available upstream_oauth2 options including auth_style.

3. Run your MCP server

Your MCP server will receive the upstream provider's access token in the Authorization: Bearer header of every proxied request. Use this token to call the upstream API directly.

docker compose up -d

4. Connect a client

When an MCP client connects, the user will be prompted to:

  1. Sign in to Pomerium (identity provider)
  2. Authorize with the upstream OAuth provider (e.g., GitHub)

After both steps, the client receives an external token (TE) and can make tool calls normally.

Common upstream providers

ProviderAuth URLToken URLCommon Scopes
GitHubhttps://github.com/login/oauth/authorizehttps://github.com/login/oauth/access_tokenread:user, repo
Googlehttps://accounts.google.com/o/oauth2/authhttps://oauth2.googleapis.com/tokenhttps://www.googleapis.com/auth/drive.readonly
Notionhttps://api.notion.com/v1/oauth/authorizehttps://api.notion.com/v1/oauth/token— (configured in integration)

Pre-Registered Credentials

Use this mode when the upstream authorization server does not support automatic client registration (neither CIMD nor DCR). You register an OAuth app manually with the provider and supply the credentials in your route config. Pomerium can still discover the authorization and token endpoints automatically via RFC 9728 PRM if the upstream advertises them — you only need to provide endpoint when it doesn't.

This is the common case for providers like Google Cloud and GitHub, where you must create an OAuth app in their console.

When registering your OAuth app, set the redirect/callback URI to https://<your-route-from-domain>/.pomerium/mcp/client/oauth/callback.

Configuration

routes:
- from: https://firestore.your-domain.com
to: https://firestore.googleapis.com/
name: Google Firestore
mcp:
server:
path: /mcp
upstream_oauth2:
client_id: <your-google-oauth-client-id>
client_secret: <your-google-oauth-client-secret>
scopes: ['https://www.googleapis.com/auth/datastore']
authorization_url_params:
access_type: 'offline'
prompt: 'consent'
policy:
allow:
and:
- domain:
is: company.com

Note the absence of endpoint — Pomerium discovers accounts.google.com as the authorization server from the upstream server's Protected Resource Metadata. You still need to add accounts.google.com to mcp_allowed_as_metadata_domains since it's a third-party domain not derivable from the route config:

mcp_allowed_as_metadata_domains:
- 'accounts.google.com'

Auto-Discovery (RFC 9728)

When the upstream MCP server advertises its own authorization requirements — rather than relying on pre-registered OAuth credentials — Pomerium can discover and negotiate the OAuth flow automatically at runtime.

This is the mode to use when connecting to third-party MCP servers where you don't register an OAuth app yourself. The upstream server tells Pomerium what authorization it needs, and Pomerium handles the rest.

How it works

  1. Pomerium forwards the client's request to the upstream MCP server
  2. The upstream server responds with 401 Unauthorized and a WWW-Authenticate header
  3. Pomerium discovers the server's Protected Resource Metadata (PRM) — either from a resource_metadata hint in the header or from well-known endpoints
  4. From the PRM, Pomerium locates the Authorization Server metadata and identifies the required scopes
  5. Pomerium identifies itself to the upstream Authorization Server using a Client ID Metadata Document (CIMD) — or falls back to Dynamic Client Registration (DCR) if the server doesn't support CIMD
  6. The user completes the upstream OAuth consent flow
  7. Pomerium caches the upstream tokens and injects them into subsequent requests

Configuration

Auto-discovery requires no upstream OAuth credentials — just define the MCP server route without an upstream_oauth2 block:

runtime_flags:
mcp: true

routes:
- from: https://notion.your-domain.com
to: https://mcp.notion.com
name: Notion
mcp:
server:
path: /mcp
policy:
allow:
and:
- domain:
is: company.com

The mcp.server block without upstream_oauth2 tells Pomerium to use auto-discovery. Pomerium will discover the server's authorization requirements, register itself as an OAuth client, and manage the full token lifecycle.

Optional settings:

SettingDescription
mcp.server.pathSub-path appended to the upstream URL for the MCP endpoint (e.g., /mcp)
mcp.server.authorization_server_urlFallback Authorization Server URL if PRM discovery fails. Must be HTTPS.

Global settings for auto-discovery:

Auto-discovery involves fetching metadata documents from URLs that originate in upstream server responses. To protect against SSRF, Pomerium validates these URLs against two domain allowlists:

# Domains allowed to serve Client ID Metadata Documents (CIMD)
mcp_allowed_client_id_domains:
- 'vscode.dev'
- '*.trusted-provider.com'

# Domains allowed for upstream Authorization Server and Protected Resource Metadata fetches
mcp_allowed_as_metadata_domains:
- 'auth.example.com'
- '*.oauth-provider.com'
SettingDescription
mcp_allowed_client_id_domainsDomains that may serve Client ID Metadata Documents. Required when MCP clients use URL-based client IDs. Supports wildcards (e.g. *.example.com).
mcp_allowed_as_metadata_domainsDomains Pomerium may contact during upstream OAuth discovery — this includes resource_metadata URLs from WWW-Authenticate headers and authorization_servers entries from PRM documents. Supports wildcards.

Step-by-step

1. Add the route to Pomerium

Configure the MCP server route as shown above. The key difference from static OAuth2 is the absence of the upstream_oauth2 block — Pomerium discovers everything it needs from the server's metadata.

If you know the Authorization Server URL but PRM discovery may not be available, you can set mcp.server.authorization_server_url as a fallback.

2. Connect a client

When an MCP client connects, the user will be prompted to:

  1. Sign in to Pomerium (identity provider)
  2. Authorize with the upstream MCP server's OAuth provider

The first request to the upstream server triggers discovery. After the user completes both auth steps, Pomerium caches the upstream tokens and all subsequent requests are authenticated transparently.

Sample repos and next steps

Feedback