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: Bearerheader 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:
- Pre-registered credentials — use when the upstream authorization server requires manual app registration and does not support automatic client registration. You supply
client_idandclient_secret(and optionally endpoints) in your route config. - 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. - 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
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_'
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:
- Go to Settings → Developer settings → OAuth Apps → New OAuth App
- Set the authorization callback URL to
https://<your-route-from-domain>/.pomerium/mcp/client/oauth/callback— for example, if your route'sfromishttps://github.your-domain.com, the callback URL ishttps://github.your-domain.com/.pomerium/mcp/client/oauth/callback - Note the Client ID and Client Secret
For other providers, refer to their OAuth documentation. You need:
client_idandclient_secretauth_urlandtoken_urlendpoints- The appropriate
scopesfor 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:
- Sign in to Pomerium (identity provider)
- 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
| Provider | Auth URL | Token URL | Common Scopes |
|---|---|---|---|
| GitHub | https://github.com/login/oauth/authorize | https://github.com/login/oauth/access_token | read:user, repo |
https://accounts.google.com/o/oauth2/auth | https://oauth2.googleapis.com/token | https://www.googleapis.com/auth/drive.readonly | |
| Notion | https://api.notion.com/v1/oauth/authorize | https://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
- Pomerium forwards the client's request to the upstream MCP server
- The upstream server responds with
401 Unauthorizedand aWWW-Authenticateheader - Pomerium discovers the server's Protected Resource Metadata (PRM) — either from a
resource_metadatahint in the header or from well-known endpoints - From the PRM, Pomerium locates the Authorization Server metadata and identifies the required scopes
- 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
- The user completes the upstream OAuth consent flow
- 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:
| Setting | Description |
|---|---|
mcp.server.path | Sub-path appended to the upstream URL for the MCP endpoint (e.g., /mcp) |
mcp.server.authorization_server_url | Fallback 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'
| Setting | Description |
|---|---|
mcp_allowed_client_id_domains | Domains 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_domains | Domains 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:
- Sign in to Pomerium (identity provider)
- 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
- pomerium/mcp-app-demo — Full MCP app demo with upstream OAuth integration
- Develop an MCP App — Build a UI that discovers and connects to MCP servers
- MCP Full Reference — Token types, session lifecycle, configuration details