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>/__zuplo/oauth/callback. Addhttp://localhost:9000/__zuplo/oauth/callbackfor local development withzuplo dev. If your MCP client uses127.0.0.1, use that hostname in the redirect URI too. See Local development. - 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
Attach the same MCP OAuth policy to every MCP route. Attaching different OAuth policies causes a configuration error.
Local development shortcut
To test locally without an identity provider, set browserLogin.url to the
dev-login endpoint:
Code
When browserLogin.url points at /__zuplo/oauth/dev-login, you don't need
tokenUrl, clientId, or clientSecret. The endpoint is only served on
loopback origins; production deployments cannot reach it.
The policy requires oidc.issuer and oidc.jwksUrl, but dev-login doesn't use
them. These values are placeholders; the gateway doesn't serve /jwks. Use
127.0.0.1 in your MCP client URL to match this example.
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 /__zuplo/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>/__zuplo/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