# OAuth for MCP gateways

Let people sign in to a gateway through your own identity provider instead of sharing a key.

An [MCP gateway](https://zato.io/docs/ai/mcp/index.html) with OAuth on has each person sign in with their own account at your identity provider - Microsoft Entra ID, Keycloak or any other provider that speaks OpenID Connect - instead of pasting a shared key into their AI client. The client opens a browser window for the sign-in, receives a token and sends it with every request, and the gateway verifies the token on its own against the provider's published keys. Who may call the gateway is decided by the [bearer token](https://zato.io/docs/security/bearer-tokens/index.html) definitions in the gateway's security group, and every request is recorded under the person's name in the [audit log](https://zato.io/docs/ai/mcp/audit-log.html).

The pages under [clients](https://zato.io/docs/ai/mcp/index.html#clients) say what to paste into each AI client. This page is about what happens on the gateway side and how to set up the identity provider.

## How the sign-in works {#how-the-sign-in-works}

An MCP client finds your identity provider on its own, there is nothing to configure in the client beyond the gateway's URL and, for most clients, a client ID. The sequence is the one the MCP authorization specification lays out:

1. The client sends its first request to the gateway without a token.
2. The gateway answers with HTTP 401 and a `WWW-Authenticate` header that names the gateway's metadata document:

```text
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://api.example.com/.well-known/oauth-protected-resource/mcp/billing"
```

1. The client reads the [metadata document](https://zato.io/docs/ai/mcp/oauth.html#the-metadata-document), which names the identity provider and the scopes to ask for.
2. The client reads the provider's own discovery document at `<issuer>/.well-known/openid-configuration` to find the authorization and token endpoints.
3. The client opens the browser at the provider's sign-in page, with a PKCE challenge, and the person signs in the way they always do - password, passkey, company single sign-on.
4. The provider redirects the browser back to the client's redirect URI with an authorization code and the client exchanges the code for an access token.
5. Every request from then on carries `Authorization: Bearer <token>`. The gateway verifies the signature against the provider's JWKS keys, the issuer, the audience, the expiry and the claims the bearer definition lists, without calling the provider.

Steps 1 to 6 happen once per client and provider session. A token that runs out makes the gateway answer 401 again, with `error="invalid_token"` added to the header, and the client signs in again or uses its refresh token.

## The two gateway fields {#the-two-gateway-fields}

The wizard's step 01, **How do agents connect?**, has two fields under the **Security** picker:

| Field | Meaning |
| --- | --- |
| OAuth | Whether the gateway answers unauthenticated requests with the 401 and the metadata document above. Off, the gateway refuses them with 403 and publishes no metadata, which is how a gateway secured with an API key or Basic Auth keeps working. |
| Scopes | The scopes the metadata document tells clients to ask for, separated with spaces, e.g. `openid profile` or `openid profile api://zato-mcp/.default`. Which scopes a provider needs is in its walkthrough below. |

In [enmasse](https://zato.io/docs/ai/enmasse.html), the two are the `oauth` and `oauth_scopes` keys of an `mcp_gateway` entry:

```yaml
mcp_gateway:

  - name: billing
    url_path: /mcp/billing
    oauth: true
    oauth_scopes: openid profile
    services:
      - billing.get-invoice
    security_groups:
      - mcp.billing
```

A gateway with OAuth on needs at least one bearer token definition with an issuer and an audience among its security definitions - the dashboard and enmasse both refuse to save a gateway that has OAuth on and no such definition, naming what is missing.

## The metadata document {#the-metadata-document}

Each gateway with OAuth on publishes a document in the format of RFC 9728 at `/.well-known/oauth-protected-resource` followed by the gateway's URL path, on the address the `Zato_Server_Address` environment variable gives, the same address the [export](https://zato.io/docs/ai/mcp/sharing-with-clients.html#the-externally-visible-address) uses:

```json
{
    "resource": "https://api.example.com/mcp/billing",
    "authorization_servers": ["https://login.microsoftonline.com/<tenant-id>/v2.0"],
    "scopes_supported": ["openid", "profile", "api://zato-mcp/.default"],
    "bearer_methods_supported": ["header"]
}
```

- `resource` is the gateway's own URL, which is what a client asks the provider for a token for.
- `authorization_servers` lists the issuer of every bearer token definition in the gateway's group that has an issuer and an audience - one entry when all of them come from one provider.
- `scopes_supported` is the gateway's Scopes field, one entry per scope, absent when the field is empty.

The document needs no credentials to read and may be cached for five minutes. A gateway with OAuth off answers 404 at its metadata address.

## Who gets in {#who-gets-in}

The gateway accepts a token when a bearer token definition in its security group matches it, and the definition's fields decide what matching means:

| Field | What it checks |
| --- | --- |
| Issuer | The `iss` claim equals it - the token comes from your provider and the tenant or realm you named. |
| Audience | The `aud` claim contains it - the token was issued for this gateway, or for the application that stands for your gateways, and not for another API of yours. |
| Claims | Each `name=value` entry is in the token - a list-valued claim, such as `groups` or `roles`, matches when the value is one of its elements, which is how a gateway is opened to one group of people and closed to everyone else. |
| Identity claim | Which claim names the person - see below. |

Two definitions in one group can let two different groups of people in through one gateway, each with its own [rate limit](https://zato.io/docs/ai/mcp/rate-limits.html), and a person outside both is refused even though their token is a valid one from the same provider.

A bearer token definition that verifies inbound tokens is created under **Security** > **Bearer tokens**, with the inbound fields under **Channel verification**, and added to the gateway's group under **Security** > **Groups**. In enmasse, the same definition is a `security` entry whose `username` is a label of your choice, since nothing is sent to the provider:

```yaml
security:

  - name: mcp.billing.agents
    type: bearer_token
    username: mcp-billing
    issuer: https://login.microsoftonline.com/<tenant-id>/v2.0
    audience: api://zato-mcp
    identity_claim: preferred_username
    claims:
      - groups=<group-object-id>

groups:

  - name: mcp.billing
    members:
      - mcp.billing.agents
```

## The identity claim {#the-identity-claim}

Without an identity claim, every person whose token matches a definition is one caller to the gateway - the definition itself. With one, the person's identity is the definition's name, a slash and the value of that claim in the token, e.g. `mcp.billing.agents/maria.johnson@example.com`, and that is the identity the gateway uses everywhere:

- MCP sessions belong to the person who opened them - a session ID presented with another person's token is refused with HTTP 400, whether or not the session exists.
- [Rate limits](https://zato.io/docs/ai/mcp/rate-limits.html) on the definition count each person separately.
- The [audit log](https://zato.io/docs/ai/mcp/audit-log.html) records the identity as the caller of every request and shows it in the **Identity** column.

The claim to name depends on the provider - `preferred_username` for Entra ID and Keycloak, `email` where every account has one, `sub` for a stable identifier that is not a name. In the dashboard, the field is **Identity claim** under **Channel verification**, in enmasse it is `identity_claim`.

## What a refused person sees {#what-a-refused-person-sees}

A person whose token the gateway refuses is told so by their client - the gateway answers 401 with `error="invalid_token"` in the challenge, the client reports that the server rejected the credentials and, depending on the product, offers to sign in again. A person whose token is fine but who is outside the group the definition lists sees the same refusal, since to the client the two are one answer.

What the difference was is in the audit log, with the audit log on for the gateway. Each refusal is an `auth-failed` event whose **Reason** column says why - `expired`, `wrong_audience`, `wrong_issuer`, `unknown_key`, `claim_missing`, `claim_mismatch` and the rest of the vocabulary the [audit log](https://zato.io/docs/ai/mcp/audit-log.html#the-caller-and-the-auth-block) page lists - and, when the token could be read, whose **Identity** and **Client** columns say who it was and which client they used. The server log carries the same reason next to the definition's name and the correlation ID.

## Microsoft Entra ID {#microsoft-entra-id}

Entra ID issues tokens for an application you register as the API, and each AI client is an application of its own that asks for a token for that API. The person managing the tenant does this once:

1. Register the API - **App registrations** > **New registration**, e.g. `Zato MCP`. Under **Expose an API**, set the Application ID URI, e.g. `api://zato-mcp`, and add a scope, e.g. `access`, with admin and user consent. Under **Manifest**, set `requestedAccessTokenVersion` - `accessTokenAcceptedVersion` in the older manifest format - to `2`, so that the tokens carry the v2.0 issuer and the claims below.
2. Decide how to tell people apart. Under **Token configuration**, add the **groups** claim with security groups, which puts the object IDs of a person's groups into the token, or define **App roles** on the API registration and assign them to people and groups, which puts the role names into a `roles` claim.
3. Register each AI client as a public client - one registration per product, e.g. `Zato MCP - VS Code`. Under **Authentication**, add the **Mobile and desktop applications** platform with that product's redirect URI, taken from its [client page](https://zato.io/docs/ai/mcp/index.html#clients), and set **Allow public client flows** to **Yes**. Under **API permissions**, add the API's scope from step 1 and `offline_access`, so that the client can refresh tokens without a new sign-in, then grant admin consent. The registration's **Application (client) ID** is the client ID the person enters in the product.
4. Copilot Studio and ChatGPT are confidential applications instead - their registrations have a client secret under **Certificates and secrets** and the **Web** platform with the callback URL each product shows when the connection is created, as described on the [Microsoft 365 Copilot](https://zato.io/docs/ai/mcp/clients/microsoft-365-copilot.html) and [ChatGPT](https://zato.io/docs/ai/mcp/clients/chatgpt.html) pages.

The gateway's fields that follow:

| Field | Value |
| --- | --- |
| Scopes on the gateway | `openid profile offline_access api://zato-mcp/access` |
| Issuer on the definition | `https://login.microsoftonline.com/<tenant-id>/v2.0` |
| Audience on the definition | `api://zato-mcp`, the Application ID URI from step 1 |
| Claims on the definition | `groups=<group-object-id>` with the groups claim, or `roles=<role-name>` with app roles |
| Identity claim on the definition | `preferred_username` |

The JWKS URL stays empty - the issuer's discovery document names it.

## Keycloak {#keycloak}

Keycloak issues tokens for a realm, and whether each AI client needs a registration of its own depends on one realm setting. The person managing the realm does this once:

1. Create a realm, e.g. `company`, or use an existing one. People and groups live there - a group such as `billing-agents` is what the gateway's definition lists.
2. Put the audience into tokens - Keycloak does not add one by default. Under **Client scopes**, create a scope, e.g. `zato-mcp`, add a mapper of type **Audience** with the **Included Custom Audience** set to the value you will put in the definition, e.g. `zato-mcp`, and a mapper of type **Group Membership** with **Token Claim Name** `groups` and **Full group path** off. Under **Client scopes** > **Default client scopes** of the realm, add the scope as a default one, so that every client's tokens carry both claims.
3. Either register each AI client - **Clients** > **Create client**, with **Client authentication** off, **Standard flow** on, the product's redirect URI from its [client page](https://zato.io/docs/ai/mcp/index.html#clients) under **Valid redirect URIs** and **Proof Key for Code Exchange Code Challenge Method** set to `S256` under **Advanced** - and give the person its client ID.
4. Or turn dynamic client registration on, in which case clients that support it, ChatGPT among them, register themselves and no per-client step is needed. Under **Clients** > **Client registration** > **Anonymous access policies**, remove the **Trusted Hosts** and **Consent Required** policies, or adjust **Trusted Hosts** to the addresses the clients register from. Clients that need a client ID ahead of time, VS Code and Cursor among them, still take one from step 3.

The gateway's fields that follow:

| Field | Value |
| --- | --- |
| Scopes on the gateway | `openid profile` |
| Issuer on the definition | `https://keycloak.example.com/realms/company` |
| Audience on the definition | `zato-mcp`, the audience from step 2 |
| Claims on the definition | `groups=billing-agents` |
| Identity claim on the definition | `preferred_username` |

The JWKS URL stays empty here too.

## Other clients {#other-clients}

Any client that implements MCP authorization - the 401 with protected resource metadata, the provider's discovery document and the authorization code flow with PKCE - connects to a gateway the way the clients with pages of their own do, with a pre-registered client ID or with dynamic registration where the provider supports it. Codex, Gemini CLI, Agentforce, ServiceNow, Slackbot, Gemini Enterprise, Glean and SAP Joule each describe such a connection in their own documentation, with a URL field and, where the product has one, a client ID field, and the redirect URI each needs is in that documentation as well.

## See also {#see-also}

- [Security](https://zato.io/docs/ai/mcp/security.html) - The credential types a gateway accepts and how rejections are answered
- [Bearer tokens](https://zato.io/docs/security/bearer-tokens/index.html) - The definitions whose issuer, audience, claims and identity claim decide who gets in
- [Audit log](https://zato.io/docs/ai/mcp/audit-log.html) - The person, the client and the reason recorded with every request
- [Sharing with clients](https://zato.io/docs/ai/mcp/sharing-with-clients.html) - The export that names the metadata document and the scopes
- [MCP gateways](https://zato.io/docs/ai/mcp/index.html) - Configuration, the pages for each client and the governance controls

## Learn more {#learn-more}

- [MCP tutorial](https://zato.io/tutorials/mcp/01.html) - Expose Python services as AI tools and connect Claude Code in minutes
- [AI Integrations overview](https://zato.io/docs/ai/) - Both directions of AI traffic in one platform, and where to start
- [MCP gateway reference](https://zato.io/docs/ai/mcp/) - Configuration, endpoint behavior and the governance controls
- [Tool schemas](https://zato.io/docs/ai/mcp/tool-schemas.html) - How a service's docstring and declared I/O become its tool definition
- [MCP gateway security](https://zato.io/docs/ai/mcp/security.html) - API keys, Basic Auth and bearer tokens for AI agents
- [MCP response controls](https://zato.io/docs/ai/mcp/response-controls.html) - PII removal, prompt-injection safeguards and token-denominated size caps
- [MCP audit log](https://zato.io/docs/ai/mcp/audit-log.html) - What each agent did, when and with what outcome - payloads never recorded
- [MCP alerts](https://zato.io/docs/ai/mcp/alerts.html) - Failing backends, agents stuck in a loop and gateways with too many tools
- [Cost and limits](https://zato.io/docs/ai/cost-and-limits.html) - Every knob that caps AI cost and traffic, with worked examples
- [GitOps for AI integrations](https://zato.io/docs/ai/enmasse.html) - LLM connections and MCP gateways as YAML in version control
- [AI examples](https://zato.io/docs/ai/examples/) - Complete, runnable scenarios - one real-world need per file
