ZuploZuplo
LoginStart for Free
  • Documentation
  • API Reference
Introduction
Getting Started
    Develop using the Portal
      1 - Setup Your Gateway2 - Rate Limiting3 - API Key Auth4 - Deploy5 - Dynamic Rate LimitingMCP - Quick start
    Develop Locally
      1 - Setup Your Gateway2 - Rate Limiting3 - API Key Auth
Concepts
Development
Policies
Handlers
API Keys
MCP Server
MCP Gateway
    IntroductionBetaQuickstartQuickstart (Local Dev)How it works
    Connect MCP clients
    Authentication
      OverviewUpstream OAuthConnect an upstream OAuth providerConnect an API-key upstream
      Identity providers
        Auth0Amazon CognitoClerkMicrosoft EntraGoogleKeycloakLogtoOktaOneLoginPingOneWorkOSGeneric OIDC
      Manual OAuth testing
    Configuration
    Observability
    ReferenceTroubleshooting
AI Gateway
Developer Portal
Monetization
Deploying & Source Control
Observability
Networking & Infrastructure
Account Management
Programming API
Build with AI
Zuplo CLI
Migration Guides
Platform LimitsSecuritySupportTrust & ComplianceChangelog
powered by Zudoku
Identity providers

Configuring a generic OIDC provider

The mcp-oauth-inbound policy is the catch-all for OIDC identity providers that don't yet have a first-class wrapper. It accepts the OIDC URLs explicitly and otherwise behaves the same as every per-provider wrapper.

Use this policy when your IdP doesn't appear in the provider catalog. Common cases:

  • Ory Hydra — self-hosted OAuth 2.0/OIDC.
  • Authentik — open-source IdP.
  • ZITADEL — open-source IdP.
  • FusionAuth — self-hosted IdP.
  • PingFederate — enterprise IdP (use this policy, not mcp-ping-oauth-inbound, which is for PingOne cloud).
  • A custom OIDC server you operate yourself.

If your IdP is on the catalog, use the dedicated wrapper instead — it validates provider-specific inputs at boot.

Read the authentication overview first for the two-layer model.

What the gateway needs from your IdP

The gateway needs three pieces of information about your IdP:

  1. The OIDC issuer URL — the value of iss in ID tokens.
  2. The JWKS URL — where the gateway fetches the IdP's public keys to verify ID tokens.
  3. The authorize URL — where the gateway redirects the user's browser to log in.

For the federated authorization-code exchange you also need a token URL, a client ID, and a client secret. The options reference below lists every field.

Most OIDC providers publish all four URLs in a discovery document at {issuer}/.well-known/openid-configuration. Fetch that document in a browser to copy the values.

Set up the OIDC application

Each IdP exposes its application registration differently, but every flow lands at the same place:

  1. Create a new OIDC web application (or "regular web application", "OIDC client", "confidential client" — terminology varies).
  2. Set the redirect URI to https://<gateway-host>/oauth/callback. Add http://localhost:9000/oauth/callback for local development with zuplo dev.
  3. Note the client ID and client secret.
  4. Restrict the application to the users or groups who should be able to authenticate against the gateway.

Wire the policy into the gateway

Add the policy to config/policies.json:

Code
{ "name": "oidc-managed-oauth", "policyType": "mcp-oauth-inbound", "handler": { "module": "$import(@zuplo/runtime/mcp-gateway)", "export": "McpOAuthInboundPolicy", "options": { "oidc": { "issuer": "https://idp.example.com", "jwksUrl": "https://idp.example.com/.well-known/jwks.json" }, "browserLogin": { "url": "https://idp.example.com/oauth2/authorize", "tokenUrl": "https://idp.example.com/oauth2/token", "clientId": "$env(OIDC_CLIENT_ID)", "clientSecret": "$env(OIDC_CLIENT_SECRET)" } } } }

Set OIDC_CLIENT_ID and OIDC_CLIENT_SECRET in your project's environment configuration (the secret goes in the secret store).

Attach the policy to each MCP route in config/routes.oas.json:

Code
{ "paths": { "/mcp/linear-v1": { "get,post": { "operationId": "linear-mcp-server", "x-zuplo-route": { "corsPolicy": "none", "handler": { "module": "$import(@zuplo/runtime/mcp-gateway)", "export": "McpProxyHandler", "options": { "rewritePattern": "https://mcp.linear.app/mcp", }, }, "policies": { "inbound": ["oidc-managed-oauth", "mcp-token-exchange-linear"], }, }, }, }, }, }

Register the gateway plugin in modules/zuplo.runtime.ts:

Code
import { RuntimeExtensions } from "@zuplo/runtime"; import { McpGatewayPlugin } from "@zuplo/runtime/mcp-gateway"; export function runtimeInit(runtime: RuntimeExtensions) { runtime.addPlugin(new McpGatewayPlugin()); }

One MCP OAuth policy serves every MCP route in the project. The gateway rejects projects that declare more than one MCP OAuth policy.

Local development shortcut

For local development without round-tripping a real IdP, set browserLogin.url to the loopback dev-login endpoint:

Code
{ "options": { "oidc": { "issuer": "http://localhost:9000/", "jwksUrl": "http://localhost:9000/dev/jwks" }, "browserLogin": { "url": "http://127.0.0.1:9000/oauth/dev-login" } } }

When browserLogin.url points at /oauth/dev-login, you don't need tokenUrl, clientId, or clientSecret. The endpoint is only served on loopback origins; production deployments cannot reach it.

See the local development guide for the rest of the local setup.

Full options reference

mcp-oauth-inbound has two required option groups: oidc and browserLogin.

OptionRequiredDefaultNotes
oidc.issueryes—The OIDC issuer URL. Must include the scheme.
oidc.jwksUrlyes—JWKS endpoint that publishes the IdP's signing keys.
oidc.audiencenounsetOptional ID-token audience override. Leave unset when ID tokens use the OIDC client_id as their audience.
browserLogin.urlyes—The IdP's /authorize endpoint. The loopback /oauth/dev-login shortcut works for local dev.
browserLogin.tokenUrlfor federated OIDC—The IdP's token endpoint. Required for the federated authorization-code exchange.
browserLogin.clientIdfor federated OIDC—OIDC client_id registered with the IdP.
browserLogin.clientSecretfor federated OIDC—OIDC client_secret. Use $env(...).
browserLogin.scopenoopenid profile emailOIDC scopes requested during browser login.
browserLogin.audiencenounsetOptional audience parameter for Auth0-style API audiences.
browserLogin.remoteTimeoutMsno10000Outbound timeout for IdP calls.
browserLogin.stateTtlSecondsno900Browser-login state record lifetime.
browserLogin.sessionTtlSecondsno28800Browser session cookie lifetime (8 hours).
gateway.accessTokenTtlSecondsno900Gateway-issued access token lifetime.
gateway.refreshTokenTtlSecondsnolong-livedGateway-issued refresh token lifetime.
gateway.cimdEnablednotrueAdvertise CIMD support in AS metadata.

Notes for specific providers

  • Ory Hydra. Discovery lives at {issuer}/.well-known/openid-configuration; set the issuer to the public-facing Hydra URL.
  • Authentik. The issuer is https://<authentik-host>/application/o/<slug>/ (note the trailing slash). The metadata document is at that issuer plus .well-known/openid-configuration.
  • ZITADEL. The issuer is your ZITADEL custom domain; metadata is at {issuer}/.well-known/openid-configuration.
  • FusionAuth. The issuer is your FusionAuth host; metadata is at {issuer}/.well-known/openid-configuration.
  • PingFederate. Use this generic policy (not the PingOne wrapper). PingFederate deployments can customize issuer hosts, issuer paths, and endpoint paths; copy the four URLs from your federation metadata.
  • Google Workspace. Google has a first-class wrapper — Configuring Google.
  • Microsoft Entra ID. Entra has a first-class wrapper — Configuring Microsoft Entra.
  • Keycloak. Keycloak has a first-class wrapper — Configuring Keycloak.

In every case, the gateway only needs the four URL fields (issuer, JWKS, authorize, token) plus a client ID and secret.

Test the configuration

The fastest sanity check is to connect an MCP client:

  1. Open Claude Desktop, Cursor, Claude Code, or another OAuth-aware MCP client.
  2. Add a remote MCP server pointing at one of your /mcp/{slug} routes.
  3. The client should redirect you to your IdP's login page. After login, the gateway's consent screen renders. Approve it.
  4. The client receives an access token and can call tools/list.

If something fails partway through, walk the flow manually using the manual OAuth testing guide — it exercises every endpoint with curl so you can see the raw responses.

Common issues

  • The gateway returns 500 at boot. A required option is missing or invalid. Check the runtime logs for the configuration error.
  • ID token verification fails. The oidc.jwksUrl doesn't match the IdP's actual JWKS endpoint, or the IdP rotated keys. Restart the gateway to clear the JWKS cache.
  • invalid_audience from the gateway's token endpoint. The MCP client is reusing a token bound to a different route. Each gateway-issued token is scoped to one MCP route.
  • MCP client can't discover the AS. Confirm the mcp-oauth-inbound policy is attached to the route in routes.oas.json and the McpGatewayPlugin is registered in modules/zuplo.runtime.ts.
  • Browser login redirects but the callback fails. The https://<gateway-host>/oauth/callback URL isn't on the application's redirect URI allow-list at the IdP.

Related

  • Authentication overview — the provider catalog and the two-layer OAuth model.
  • Per-user OAuth to upstream MCP servers
  • Manual OAuth testing
Edit this page
Last modified on May 27, 2026
WorkOSManual OAuth testing
On this page
  • What the gateway needs from your IdP
  • Set up the OIDC application
  • Wire the policy into the gateway
  • Local development shortcut
  • Full options reference
  • Notes for specific providers
  • Test the configuration
  • Common issues
  • Related
JSON
JSON
TypeScript
JSON