Configure OAuth 2.1 discovery
Atoti Intelligence SDK
This is part of the Atoti Intelligence SDK offer.
This guide explains how to enable the OAuth 2.1 / PKCE browser-SSO flow on the Atoti MCP server,
so MCP clients (Claude Code, Claude Desktop, Cursor, Cline, VS Code’s MCP support, Gemini CLI)
can drive authentication against your IdP without users copy-pasting Bearer tokens.
What this enables
When this feature is enabled, the MCP server:
- Publishes an RFC 9728
Protected Resource Metadata document at
/.well-known/oauth-protected-resource. The
document tells clients which authorization server(s) to talk to and which scopes to request.
- Returns
WWW-Authenticate: Bearer realm="mcp", resource_metadata="..." on 401 responses
from /mcp/** and /sse, so clients can discover the metadata document on first contact.
Compliant MCP clients then drive the standard
OAuth 2.1 authorization code + PKCE
flow against the IdP: the client opens the system browser at the IdP authorization endpoint, the
user signs in (with MFA, conditional access, whatever the IdP enforces), the IdP redirects back to
a loopback URI, and the client swaps the code for an access token. The token is stored in the OS
keychain and refreshed silently in the background. Users SSO once in a browser instead of
copy-pasting tokens into their MCP client configuration.
Prerequisites
- The Atoti MCP server is already running. See
Atoti MCP server setup guide.
- An OAuth 2.0 authorization server (Okta, Entra ID, Auth0, Keycloak, Cognito, …) is configured
to issue access tokens for users who should be able to call MCP endpoints.
- The Atoti Spring Security stack is already configured to validate the IdP’s JWTs (this is the
same setup used by other Bearer-secured Atoti endpoints — no extra steps).
Enable the feature
Add the following to your application.yml:
atoti:
server:
endpoint:
mcp:
oauth2:
enabled: true
authorization-servers:
- https://idp.example.com/realms/atoti
scopes-supported:
- mcp.read
- mcp.write
authorization-servers is the only required setting when enabled=true. Startup fails with a
clear error if it is left empty.
Property reference
| Property | Type | Default | Description |
|---|
atoti.server.endpoint.mcp.oauth2.enabled | boolean | false | Master switch. When false, none of the discovery beans are registered and MCP endpoints behave exactly as before. |
atoti.server.endpoint.mcp.oauth2.resource | string (URI) | request-derived | Absolute URI identifying this MCP server as an OAuth 2.0 resource. When unset, derived from the request’s context path at serve time. Set this explicitly if the server is behind a reverse proxy that rewrites URLs. |
atoti.server.endpoint.mcp.oauth2.authorization-servers | list of URIs | [] | URLs of trusted authorization servers. At least one is required when the feature is enabled. |
atoti.server.endpoint.mcp.oauth2.scopes-supported | list of strings | [] | Optional OAuth 2.0 scopes advertised to clients. Omitted from the payload when empty. |
atoti.server.endpoint.mcp.oauth2.bearer-methods-supported | list of strings | ["header"] | Supported Bearer token transport methods per RFC 6750. |
atoti.server.endpoint.mcp.oauth2.resource-documentation | string (URI) | unset | Optional URL pointing at human-readable documentation for the resource. |
atoti.server.endpoint.mcp.oauth2.realm | string | "mcp" | Realm used in the WWW-Authenticate: Bearer realm="..." challenge. |
atoti.server.endpoint.mcp.oauth2.well-known-path | string | /.well-known/oauth-protected-resource | Path under which the metadata document is served. Override only if another resource server on the same origin already owns the default path. |
What the authorization server must provide
Role requirement
The /mcp/** and /sse endpoints require the authenticated user to hold at least one of the
deployment’s configured Atoti admin or user roles. The defaults are ROLE_USER and ROLE_ADMIN,
controlled by atoti.server.security.roles-user and atoti.server.security.roles-admin.
The IdP-issued access token must carry the user’s roles in a claim that the deployment’s existing
OIDC/JWT role mapping translates into Atoti authorities. This is the same role-mapping mechanism
used by other Bearer-secured Atoti endpoints: no extra configuration is needed for MCP
specifically, but the mapping must be in place.
A missing or incomplete role mapping causes HTTP 403 after a successful login. The token is valid
and accepted, but the user holds no Atoti authority. Add the role claim to the issued token and
verify the deployment’s role-mapping configuration before concluding that MCP OAuth is broken.
Client registration
MCP clients are public OAuth clients. They use the authorization code flow with PKCE (S256) and
redirect to a loopback URI (for example, http://127.0.0.1:<random-port>/callback).
Register the MCP client in the IdP as:
- A public client with no client secret.
- PKCE enabled (S256 required).
- Loopback redirect URIs allowed. The client opens a random port on
127.0.0.1 for the
callback. The IdP must permit this pattern (for example, http://127.0.0.1:* or an equivalent
loopback allowlist). The MCP Authorization spec mandates loopback redirect URI support for
native clients.
A confidential client (one that requires a client secret) does not fit the MCP client model.
Audience (optional)
The atoti.server.endpoint.mcp.oauth2.resource property identifies this MCP server as an OAuth
2.0 resource. Atoti does not validate the aud claim by default: a token without a matching
audience passes validation. Audience validation applies only if the deployment has explicitly
configured an allowed audience in its JWT resource-server settings.
Set the audience on issued tokens only if the deployment is already configured to require it.
Dynamic Client Registration
Some MCP clients use Dynamic Client Registration (RFC 7591)
instead of a pre-registered client. The IdP must permit anonymous registration requests from the
client’s host.
Even when DCR succeeds, the connection can still fail at /mcp with HTTP 403 if the dynamically
registered client’s tokens do not carry the user’s roles. See the
role requirement section above.
Keycloak example
The following Keycloak policies affect DCR and are relevant when connecting MCP clients.
Trusted Hosts policy. Keycloak’s default DCR configuration blocks registrations from
untrusted hosts with the error “Host not trusted”. Add the loopback address or the registration
host to the Trusted Hosts policy in the realm’s Client Registration settings.
Full Scope Disabled policy. Keycloak’s default DCR configuration sets fullScopeAllowed=false
on registered clients. This strips the user’s client roles from the issued token and causes HTTP
403 at /mcp. Remove or relax the Full Scope Disabled policy so that dynamically registered
clients receive the user’s roles in their tokens.
Both policies are configured in the Keycloak Admin Console under the realm’s Client
Registration settings. They are IdP-side settings and require no changes to Atoti.
Troubleshooting: 401 vs 403
| Status | Meaning | What to check |
|---|
401 Unauthorized | The token was not accepted. | Verify the IdP issuer URI matches what Atoti trusts. Check token expiry and signature. |
403 Forbidden | The user authenticated but holds no required Atoti authority. | Confirm the token carries roles in the mapped claim. Verify the deployment’s OIDC/JWT role mapping. If DCR is used, check the Full Scope Disabled policy. |
Per-client UX
- Claude Code, Claude Desktop, Cursor, Cline, VS Code MCP, Gemini CLI: detect the
WWW-Authenticate header automatically, open the system browser for the PKCE flow, store the
resulting access and refresh tokens in the OS keychain, and refresh silently.
- Older or stdio-only MCP clients: use the
mcp-remote bridge — it performs the PKCE dance locally
and proxies SSE to the remote MCP server.
Reverse-proxy caveats
If the Atoti server sits behind a reverse proxy that rewrites the host or scheme:
- Either set
atoti.server.endpoint.mcp.oauth2.resource explicitly to the public URL of the
server, or
- Enable Spring Boot’s
ForwardedHeaderFilter (via server.forward-headers-strategy=framework)
so the request-derived URL honours X-Forwarded-*.
Without one of these, the resource field and the resource_metadata= URL in the challenge
header will reflect the internal hostname instead of the public one.
Limitations and out-of-scope features
In external mode the MCP server is only a resource server — it advertises discovery metadata and
validates Bearer tokens, but it does not issue them. The following are therefore out of scope for
external mode (they are the IdP’s responsibility):
- Dynamic Client Registration (RFC 7591). Customers pre-register MCP clients in their IdP, or
rely on the IdP’s own DCR support.
/.well-known/oauth-authorization-server. That endpoint is served by the IdP.
If you want Atoti itself to handle client registration and serve the authorization-server metadata,
use self-issued mode instead — it implements both.
The following are not implemented in either mode in this release:
- Token Exchange (RFC 8693). The MCP server does not exchange tokens for downstream
services.
- Per-error-code
WWW-Authenticate parameters (error=invalid_token, etc., per RFC 6750).
The v1 challenge contains only realm and resource_metadata.