# Configuring Auth0

The MCP Gateway can use Auth0 as the identity provider behind its downstream
OAuth flow. The `mcp-auth0-oauth-inbound` policy is an Auth0-friendly wrapper
around the generic `mcp-oauth-inbound` policy: provide your Auth0 domain, a
client ID, and a client secret, and the policy derives the OIDC issuer, JWKS
URL, and Auth0 authorize and token URLs for you.

This guide walks through the Auth0 dashboard setup, then shows how to wire the
policy into your gateway project. Read the
[authentication overview](./overview.mdx) first for the two-layer model and the
role each policy plays.

:::note

This guide assumes you have a working Auth0 tenant. The
[Auth0 MCP client registration guide](https://auth0.com/ai/docs/mcp/guides/registering-your-mcp-client-application)
is the authoritative source for Auth0-side configuration.

:::

## How the wrapper derives configuration

Given a single `auth0Domain` like `my-tenant.us.auth0.com`, the wrapper derives
every URL the generic policy needs:

| Underlying option       | Derived value                                          |
| ----------------------- | ------------------------------------------------------ |
| `oidc.issuer`           | `https://my-tenant.us.auth0.com/`                      |
| `oidc.jwksUrl`          | `https://my-tenant.us.auth0.com/.well-known/jwks.json` |
| `browserLogin.url`      | `https://my-tenant.us.auth0.com/authorize`             |
| `browserLogin.tokenUrl` | `https://my-tenant.us.auth0.com/oauth/token`           |

That's why most projects need only three fields: `auth0Domain`, `clientId`, and
`clientSecret`. The `audience`, `scope`, and TTL options are all optional
overrides.

:::caution

`auth0Domain` is a **bare hostname**, not a URL. The policy rejects values that
include `http://` or `https://` prefixes or that don't contain a dot. Use
`my-tenant.us.auth0.com`, not `https://my-tenant.us.auth0.com/`.

:::

## Set up the Auth0 tenant

The MCP Gateway acts as an OAuth 2.1 authorization server in front of Auth0.
Auth0 handles browser login and identity; the gateway issues its own access
tokens that bind to MCP routes. The Auth0 application you create represents the
**gateway's identity** against Auth0, not the MCP client.

### Create an Auth0 application

1. In the Auth0 Dashboard, open **Applications > Applications** and click
   **Create Application**.
2. Set a name (for example, `Zuplo MCP Gateway`).
3. Choose **Regular Web Application** as the application type and click
   **Create**.
4. On the **Settings** tab, note the **Domain**, **Client ID**, and **Client
   Secret**. You'll wire these into the policy in the next section.

### Configure callback and origin URLs

The gateway completes browser login by redirecting back to its own
`/oauth/callback` endpoint, so Auth0 needs that URL on its allow-list.

On the same **Settings** tab:

1. Set **Allowed Callback URLs** to your gateway's
   `https://<gateway-host>/oauth/callback`. For local development against
   `zuplo dev`, add `http://localhost:9000/oauth/callback` as well.
2. Set **Allowed Web Origins** to the gateway origin `https://<gateway-host>`
   (plus `http://localhost:9000` for local dev).
3. Save changes.

### Optional: Set an audience

If you want Auth0 to issue identity-bound API access tokens (for example, to
validate Auth0-issued tokens against a specific resource server), create an API
under **Applications > APIs** with an identifier like
`https://gateway.example.com` and pass that identifier as the `audience` option
on the policy. When omitted, Auth0 acts only as the browser identity layer and
the gateway alone owns the OAuth grant the MCP client receives.

### Connections and dynamic client registration

The downstream OAuth flow only requires Auth0 to authenticate the user and
return an ID token. CIMD and DCR on Auth0's side concern the **upstream MCP
server's** trust of clients, not the gateway's trust of Auth0. If you also
configure Auth0 itself as an upstream MCP authorization provider (rare), follow
Auth0's own guide for
[enabling CIMD](https://auth0.com/ai/docs/mcp/guides/registering-your-mcp-client-application/manual-cimd-registration)
or
[enabling DCR](https://auth0.com/ai/docs/mcp/guides/registering-your-mcp-client-application/dynamic-client-registration).

## Wire the policy into the gateway

Add the policy to `config/policies.json`:

```json
{
  "name": "auth0-managed-oauth",
  "policyType": "mcp-auth0-oauth-inbound",
  "handler": {
    "module": "$import(@zuplo/runtime/mcp-gateway)",
    "export": "McpAuth0OAuthInboundPolicy",
    "options": {
      "auth0Domain": "$env(AUTH0_DOMAIN)",
      "clientId": "$env(AUTH0_CLIENT_ID)",
      "clientSecret": "$env(AUTH0_CLIENT_SECRET)"
    }
  }
}
```

Set the three environment variables in your Zuplo project's environment
configuration. `AUTH0_DOMAIN` is the bare hostname; the secret values belong in
the project secret store.

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

```jsonc
{
  "paths": {
    "/mcp/linear": {
      "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": ["auth0-managed-oauth", "mcp-token-exchange-linear"],
          },
        },
      },
    },
  },
}
```

Finally, register the gateway plugin in `modules/zuplo.runtime.ts` so the
runtime registers the OAuth endpoints automatically:

```ts
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 — there's no need to
declare it more than once. Attaching the same policy by name to each route is
the canonical pattern.

## Full options reference

`mcp-auth0-oauth-inbound` has three required options and a few optional
overrides. The complete schema is documented on the policy reference page; the
fields you'll touch most often are:

| Option                                    | Required | Default                | Notes                                                                                      |
| ----------------------------------------- | -------- | ---------------------- | ------------------------------------------------------------------------------------------ |
| `auth0Domain`                             | yes      | —                      | Bare hostname (`my-tenant.us.auth0.com`). No scheme, must contain a dot.                   |
| `clientId`                                | yes      | —                      | Auth0 application client ID.                                                               |
| `clientSecret`                            | yes      | —                      | Auth0 application client secret. Use `$env(...)` to source from a secret.                  |
| `audience`                                | no       | unset                  | Optional Auth0 API identifier. Sent as the `?audience=` parameter to Auth0's `/authorize`. |
| `scope`                                   | no       | `openid profile email` | OIDC scopes requested during browser login.                                                |
| `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.                                                     |
| `browserLoginOverrides.sessionTtlSeconds` | no       | `28800`                | Browser session cookie lifetime (8 hours).                                                 |
| `browserLoginOverrides.stateTtlSeconds`   | no       | `900`                  | Browser-login state record lifetime.                                                       |
| `browserLoginOverrides.remoteTimeoutMs`   | no       | `10000`                | Outbound timeout to Auth0 (token exchange, JWKS fetch).                                    |

## Test the configuration

The fastest sanity check is to try connecting 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 on the
   gateway.
3. The client should redirect you to Auth0'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](./manual-oauth-testing.mdx) — it exercises every
endpoint with `curl` so you can see the raw responses.

## Common issues

- **"Invalid Auth0 domain" at boot.** The `auth0Domain` value includes a scheme
  prefix or doesn't contain a dot. Use `my-tenant.us.auth0.com`.
- **Browser login redirects but the callback fails.** The
  `https://<gateway-host>/oauth/callback` URL isn't on the **Allowed Callback
  URLs** list for the Auth0 application.
- **Token endpoint returns `invalid_audience`.** The MCP client is reusing a
  token bound to a different route. Each gateway-issued token binds to one
  `operationId`; the client must obtain a separate token per route.
- **Issuer in AS metadata is wrong.** The gateway derives its issuer from the
  incoming request origin. Check that your custom domain or proxy forwards the
  correct `Host` or `X-Forwarded-Host` header. See
  [Troubleshooting](../troubleshooting.mdx).
- **MCP client can't discover the AS.** Confirm the `mcp-auth0-oauth-inbound`
  policy is attached to the route in `routes.oas.json` and that the
  `McpGatewayPlugin` is registered in `modules/zuplo.runtime.ts`. The internal
  OAuth endpoints register only when both are present.

## Related

- [Authentication overview](./overview.mdx)
- `mcp-auth0-oauth-inbound` policy reference
- [Configuring Okta or any other OIDC IdP](./configuring-okta.mdx)
- [Per-user OAuth to upstream MCP servers](./upstream-oauth.mdx)
