Configuring Okta (or any OIDC provider)
The mcp-oauth-inbound policy lets the MCP Gateway delegate browser login to
any OIDC-compatible identity provider. Where the
Auth0 wrapper derives all the URLs from a single
domain, the generic policy requires you to provide the OIDC URLs explicitly.
This guide uses Okta as the concrete example, but the same pattern applies to Microsoft Entra ID, Keycloak, Ory Hydra, Authentik, Google Workspace, or any other IdP that exposes RFC 8414 authorization-server metadata or OpenID Connect Discovery.
Read the authentication overview for the two-layer model and the role each policy plays before starting.
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 most IdPs you also need a token URL, a client ID, and a client secret so the gateway can complete the authorization-code exchange after browser login. The options reference lists every field.
Set up Okta
The MCP Gateway acts as an OAuth 2.1 authorization server in front of Okta. Okta handles browser login and identity; the gateway issues its own access tokens bound to MCP routes. The Okta application you create represents the gateway's identity against Okta, not the MCP client.
Create an OIDC application
- In the Okta Admin Console, open Applications > Applications and click Create App Integration.
- Choose OIDC - OpenID Connect as the sign-in method.
- Choose Web Application as the application type and click Next.
- Give the integration a name (for example,
Zuplo MCP Gateway). - Under Grant types, leave Authorization Code checked. Refresh token isn't needed by the gateway here because the gateway uses Okta only for browser identity, not as a long-running token source.
- Set Sign-in redirect URIs to your gateway's
https://<gateway-host>/oauth/callback. Addhttp://localhost:9000/oauth/callbackfor local development. - Click Save.
Note the Client ID and Client Secret from the application's General tab. You'll wire these into the policy in the next section.
Find the OIDC URLs
From the Okta Admin Console, navigate to Security > API and pick the authorization server you want to use (the default one works for most setups; a custom authorization server gives you more control over scopes and audiences).
The authorization server's metadata page shows an Issuer URI and a
Metadata URI. The issuer URI is what you pass as oidc.issuer. Fetch the
metadata URI in a browser ({issuer}/.well-known/oauth-authorization-server or
{issuer}/.well-known/openid-configuration) to find:
jwks_uri— pass this asoidc.jwksUrl.authorization_endpoint— pass this asbrowserLogin.url.token_endpoint— pass this asbrowserLogin.tokenUrl.
For the Okta default authorization server, the URLs look like:
Code
Wire the policy into the gateway
Add the policy to config/policies.json:
Code
Set OKTA_CLIENT_ID and OKTA_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 the gateway sees a loopback browserLogin.url pointing at
/oauth/dev-login, it skips tokenUrl, clientId, and clientSecret and
synthesizes a dev-browser-user subject. 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.
The complete schema is documented on the policy reference page; the fields
you'll touch most often are:
| 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 | ~10 years (runtime) | Gateway-issued refresh token lifetime. |
gateway.cimdEnabled | no | true | Advertise CIMD support in AS metadata. |
Notes for specific providers
- Microsoft Entra ID. The issuer is
https://login.microsoftonline.com/{tenant}/v2.0; the metadata document lives at{issuer}/.well-known/openid-configuration. Use the v2 endpoints. - Keycloak. The issuer is
https://<keycloak-host>/realms/<realm>; the metadata document lives at{issuer}/.well-known/openid-configuration. - Ory Hydra. Discovery lives at
{issuer}/.well-known/openid-configuration; set the issuer to the public-facing Hydra URL. - Google Workspace. Use
https://accounts.google.comas the issuer; metadata is athttps://accounts.google.com/.well-known/openid-configuration.
In all cases, the gateway only needs the four URL fields (issuer, JWKS, authorize, token) plus a client ID and secret. Discovery is performed once at startup and the JWKS is cached per the IdP's cache directives.
Test the configuration
The fastest sanity check is to try connecting 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 on the gateway. - 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 — the policy fails on the
first request with a
ConfigurationErrorthat names the broken field. - ID token verification fails. The
oidc.jwksUrldoesn't match the IdP's actual JWKS endpoint, or the IdP rotated keys and the JWKS cache is stale. Restart the gateway to clear the cache. invalid_audiencefrom the gateway's token endpoint. The MCP client is reusing a token bound to a different route. Each gateway-issued token binds to oneoperationId.- 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. The internal OAuth endpoints register only when both are present. - 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
mcp-oauth-inboundpolicy reference- Configuring Auth0
- Per-user OAuth to upstream MCP servers