OAuth for MCP gateways

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

An MCP gateway 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 definitions in the gateway's security group, and every request is recorded under the person's name in the audit log.

The pages under 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

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:
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, 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 wizard's step 01, How do agents connect?, has two fields under the Security picker:

FieldMeaning
OAuthWhether 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.
ScopesThe 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, the two are the oauth and oauth_scopes keys of an mcp_gateway entry:

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

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 uses:

{
    "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

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:

FieldWhat it checks
IssuerThe iss claim equals it - the token comes from your provider and the tenant or realm you named.
AudienceThe 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.
ClaimsEach 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 claimWhich 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, 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:

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

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 on the definition count each person separately.
  • The audit log 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

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 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

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, 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 and ChatGPT pages.

The gateway's fields that follow:

FieldValue
Scopes on the gatewayopenid profile offline_access api://zato-mcp/access
Issuer on the definitionhttps://login.microsoftonline.com/<tenant-id>/v2.0
Audience on the definitionapi://zato-mcp, the Application ID URI from step 1
Claims on the definitiongroups=<group-object-id> with the groups claim, or roles=<role-name> with app roles
Identity claim on the definitionpreferred_username

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

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 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:

FieldValue
Scopes on the gatewayopenid profile
Issuer on the definitionhttps://keycloak.example.com/realms/company
Audience on the definitionzato-mcp, the audience from step 2
Claims on the definitiongroups=billing-agents
Identity claim on the definitionpreferred_username

The JWKS URL stays empty here too.

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

FeatureWhat it does
SecurityThe credential types a gateway accepts and how rejections are answered
Bearer tokensThe definitions whose issuer, audience, claims and identity claim decide who gets in
Audit logThe person, the client and the reason recorded with every request
Sharing with clientsThe export that names the metadata document and the scopes
MCP gatewaysConfiguration, the pages for each client and the governance controls

Learn more