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:
- The OIDC issuer URL — the value of
issin ID tokens. - The JWKS URL — where the gateway fetches the IdP's public keys to verify ID tokens.
- 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:
- Create a new OIDC web application (or "regular web application", "OIDC client", "confidential client" — terminology varies).
- Set the redirect URI to
https://<gateway-host>/oauth/callback. Addhttp://localhost:9000/oauth/callbackfor local development withzuplo dev. - Note the client ID and client secret.
- 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
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
Register the gateway plugin in modules/zuplo.runtime.ts:
Code
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
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.
| Option | Required | Default | Notes |
|---|---|---|---|
oidc.issuer | yes | — | The OIDC issuer URL. Must include the scheme. |
oidc.jwksUrl | yes | — | JWKS endpoint that publishes the IdP's signing keys. |
oidc.audience | no | unset | Optional ID-token audience override. Leave unset when ID tokens use the OIDC client_id as their audience. |
browserLogin.url | yes | — | The IdP's /authorize endpoint. The loopback /oauth/dev-login shortcut works for local dev. |
browserLogin.tokenUrl | for federated OIDC | — | The IdP's token endpoint. Required for the federated authorization-code exchange. |
browserLogin.clientId | for federated OIDC | — | OIDC client_id registered with the IdP. |
browserLogin.clientSecret | for federated OIDC | — | OIDC client_secret. Use $env(...). |
browserLogin.scope | no | openid profile email | OIDC scopes requested during browser login. |
browserLogin.audience | no | unset | Optional audience parameter for Auth0-style API audiences. |
browserLogin.remoteTimeoutMs | no | 10000 | Outbound timeout for IdP calls. |
browserLogin.stateTtlSeconds | no | 900 | Browser-login state record lifetime. |
browserLogin.sessionTtlSeconds | no | 28800 | Browser session cookie lifetime (8 hours). |
gateway.accessTokenTtlSeconds | no | 900 | Gateway-issued access token lifetime. |
gateway.refreshTokenTtlSeconds | no | long-lived | Gateway-issued refresh token lifetime. |
gateway.cimdEnabled | no | true | Advertise 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:
- Open Claude Desktop, Cursor, Claude Code, or another OAuth-aware MCP client.
- Add a remote MCP server pointing at one of your
/mcp/{slug}routes. - The client should redirect you to your IdP's login page. After login, the gateway's consent screen renders. Approve it.
- 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.jwksUrldoesn't match the IdP's actual JWKS endpoint, or the IdP rotated keys. Restart the gateway to clear the JWKS cache. invalid_audiencefrom 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-inboundpolicy is attached to the route inroutes.oas.jsonand theMcpGatewayPluginis registered inmodules/zuplo.runtime.ts. - Browser login redirects but the callback fails. The
https://<gateway-host>/oauth/callbackURL 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