> ## Documentation Index
> Fetch the complete documentation index at: https://docs.activeviam.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Configure oauth2 self issued

# Configure self-issued OAuth 2.1

:::dark-info Atoti Intelligence SDK
This is part of the Atoti Intelligence SDK offer.
:::

This guide explains how to enable the `self-issued` OAuth 2.1 mode on the Atoti MCP Server, where
the Atoti server acts as its own authorization server. MCP clients (Claude Code, Claude Desktop,
Cursor, Cline, VS Code's MCP support, Gemini CLI) drive the standard browser-based PKCE flow
directly against Atoti. No external identity provider is required.

Users sign in with their existing Atoti credentials. No separate IdP, no extra user store.

## What self-issued mode is

The Atoti MCP Server supports two OAuth 2.1 modes, selected by
`atoti.server.endpoint.mcp.oauth2.mode`:

| Mode                 | Who issues tokens                                      | When to use                                                                                                                                     |
| -------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `external` (default) | A corporate IdP (Okta, Entra ID, Auth0, Keycloak, ...) | SSO, MFA, and enterprise identity management are already handled by the IdP. See [Configure OAuth 2.1 discovery](./configure-oauth2-discovery). |
| `self-issued`        | The Atoti server itself                                | The deployment authenticates users through Atoti's own user store and no external IdP is available or desired.                                  |

In `self-issued` mode the Atoti server is both the OAuth 2.1 authorization server and the resource
server. It publishes the standard metadata documents, handles Dynamic Client Registration, and
issues JWTs signed with Atoti's own keys. The MCP client's authorization flow is identical to
`external` mode; only the server the client talks to changes.

## Prerequisites

* The Atoti MCP Server is already running. See
  [Atoti MCP server setup guide](./atoti-mcp-server-setup).
* Atoti Server is configured with an RSA keypair via `atoti.jwt.key.*` (recommended). When no
  keypair is configured, an ephemeral key is generated at startup (see
  [Signing keys](#signing-keys) for the implications).

## Minimal configuration

Self-issued mode requires only four lines. Add the following to `application.yml`:

```yaml theme={"languages":{"custom":["/engine/python-sdk/0.9/languages/pycon.tmLanguage.json"]}}
atoti:
  server:
    endpoint:
      mcp:
        oauth2:
          enabled: true
          mode: self-issued
```

No authorization-server URL, no client registration, no IdP setup. Atoti handles everything.

## How the browser PKCE flow works

The following steps describe the full authorization flow as a client such as Claude Code experiences
it.

1. The client sends `POST /mcp` and receives `401` with
   `WWW-Authenticate: Bearer realm="mcp", resource_metadata="..."`.
2. The client fetches `/.well-known/oauth-protected-resource` (RFC 9728). The document advertises
   the same origin as the authorization server.
3. The client fetches `/.well-known/oauth-authorization-server` (RFC 8414). The document
   advertises `authorization_endpoint`, `token_endpoint`, `registration_endpoint`, `jwks_uri`, and
   PKCE `S256` support.
4. The client performs anonymous Dynamic Client Registration at `POST /oauth2/register` (also
   accepted at `POST /register`) and receives a generated `client_id`.
5. The client opens the system browser at `/oauth2/authorize`. The user is redirected to Atoti's
   own branded login page at `/login` and signs in with existing Atoti credentials.
6. After sign-in, the browser is shown a consent page at `/oauth2/consent` titled "Allow Atoti to
   connect to \<client name>?" listing the requested scopes (for example, `mcp.read`,
   `mcp.write`). The user clicks **Allow** or **Deny**.
7. On **Allow**, the browser redirects back to the client's loopback URI with an authorization
   code. On **Deny**, `access_denied` is returned to the client and no token is issued.
8. The client exchanges the code at `POST /oauth2/token` for a JWT access token and a refresh
   token.
9. The client calls `POST /mcp` with `Authorization: Bearer <jwt>`. Atoti validates the token and
   the user's Atoti roles authorize the requested MCP tools.

## Endpoints exposed in self-issued mode

When `mode=self-issued`, the following endpoints become active in addition to the standard
`/mcp` endpoint:

| Endpoint                                  | Purpose                                   |
| ----------------------------------------- | ----------------------------------------- |
| `/.well-known/oauth-protected-resource`   | RFC 9728 protected resource metadata      |
| `/.well-known/oauth-authorization-server` | RFC 8414 authorization server metadata    |
| `/oauth2/authorize`                       | Authorization endpoint (browser redirect) |
| `/oauth2/token`                           | Token endpoint (code exchange, refresh)   |
| `/oauth2/jwks`                            | JWKS endpoint (public signing keys)       |
| `/oauth2/register`                        | Dynamic Client Registration (RFC 7591)    |
| `/register`                               | Alias for `/oauth2/register`              |
| `/oauth2/consent`                         | Browser consent page (Allow/Deny)         |

## Key design points

### Identity and keys

Self-issued mode reuses Atoti's existing identity stack. End users authenticate against the same
user store that secures the rest of the Atoti deployment (the application's configured
`UserDetailsService` or authentication manager). The issued JWTs are signed with Atoti's own RSA
keypair, validated by Atoti's normal JWT filter, and carry the user's real Atoti roles into `/mcp`
authorization. No separate key material or user store is needed.

Self-issued mode **requires** the application to provide a `UserDetailsService` (or authentication
manager) bean. Atoti deployments already supply one — the basic-authentication fallback registers a
`UserDetailsService` whenever any authentication is configured. For local development without a full
deployment, the `apps/basic` sample application supplies an in-memory user store. If no
`UserDetailsService` is configured, the server still starts but logs a warning, and the browser
login cannot authenticate users until one is provided:
`atoti.server.endpoint.mcp.oauth2.mode=self-issued is active but no UserDetailsService is configured, so the OAuth 2.1 browser login cannot authenticate users.`

### Signing keys

The server selects the signing key in the following order of precedence:

1. **Atoti's configured RSA keypair** (recommended). Set via `atoti.jwt.key.*` or
   `activeviam.jwt.key.*`. The same key signs MCP tokens and all other Atoti JWTs.
2. **A PKCS#12 or JKS keystore**, configured via
   `atoti.server.endpoint.mcp.oauth2.authserver.jwk.keystore-location`,
   `...keystore-password`, and `...key-alias`.
3. **An ephemeral keypair**, generated at startup. This fallback is for development only: all
   issued tokens become invalid on restart, and a warning is logged at startup.

For production deployments, use option 1 or 2 so that tokens survive server restarts.

### Issuer URI

`atoti.server.endpoint.mcp.oauth2.authserver.issuer` is optional. When unset, the issuer and all
advertised absolute URLs are derived from the incoming HTTP request. Set it explicitly when the
server runs behind a reverse proxy that rewrites the host or scheme, or enable Spring Boot's
`ForwardedHeaderFilter` via `server.forward-headers-strategy=framework`.

### MCP transport

The starter configures `spring.ai.mcp.server.protocol=STREAMABLE` by default, which exposes a
single `POST /mcp` endpoint. This is the transport expected by current MCP clients. Override to
`SSE` if the deployment requires the older server-sent events transport.

### User consent

After a user authenticates, the browser is shown a consent page at `/oauth2/consent` titled
"Allow Atoti to connect to \<client name>?". The page lists the requested OAuth scopes (for
example, `mcp.read`, `mcp.write`) and presents **Allow** and **Deny** buttons.

* **Allow** completes the flow: an authorization code is issued and the client exchanges it for a
  JWT.
* **Deny** returns `access_denied` to the client; no token is issued.

Consent is requested on every authorization request, including when the user already has an active
Atoti browser session. Consent is never remembered across requests.

The consent page itself is protected by the deployment's existing human security filter chain
(the same authentication method as the rest of the Atoti UI). A browser that arrives at
`/oauth2/consent` unauthenticated is redirected to the Atoti login page first. A non-browser
caller receives `401`.

**CSRF protection.** The Allow/Deny decision is protected by a single-use session-bound consent
key: the key is generated when the gate stashes the pending authorization request, and consumed on
the subsequent `/oauth2/authorize` pass. An attacker cannot forge a consent POST without knowing
that key, which lives only in the server-side session. For production deployments, also set
`server.servlet.session.cookie.same-site=Lax` so the session cookie is not sent on cross-site
navigations, providing defence-in-depth against CSRF on the consent endpoint.

To disable the consent step and restore the previous behavior (a code is issued immediately after
authentication), set `atoti.server.endpoint.mcp.oauth2.authserver.consent.enabled` to `false`.

## Property reference

All properties are under the `atoti.server.endpoint.mcp.oauth2` prefix unless noted.

| Property                                                            | Type         | Default                 | Description                                                                                                                                                                            |
| ------------------------------------------------------------------- | ------------ | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `atoti.server.endpoint.mcp.oauth2.enabled`                          | boolean      | `false`                 | Master switch. Must be `true` to activate self-issued mode.                                                                                                                            |
| `atoti.server.endpoint.mcp.oauth2.mode`                             | enum         | `external`              | Set to `self-issued` to use the Atoti server as its own authorization server.                                                                                                          |
| `atoti.server.endpoint.mcp.oauth2.authserver.issuer`                | string (URI) | request-derived         | Issuer advertised in tokens and metadata documents. Set when the server is behind a rewriting reverse proxy.                                                                           |
| `atoti.server.endpoint.mcp.oauth2.authserver.access-token-ttl`      | duration     | `1h`                    | Lifetime of issued JWT access tokens.                                                                                                                                                  |
| `atoti.server.endpoint.mcp.oauth2.authserver.refresh-token-ttl`     | duration     | `8h`                    | Lifetime of refresh tokens.                                                                                                                                                            |
| `atoti.server.endpoint.mcp.oauth2.authserver.jwk.keystore-location` | string       | *unset*                 | Path to a PKCS#12 or JKS keystore holding the RSA signing key. When unset, Atoti's `atoti.jwt.key.*` keypair is used; when that is also absent, an ephemeral dev keypair is generated. |
| `atoti.server.endpoint.mcp.oauth2.authserver.jwk.keystore-password` | string       | *unset*                 | Password for the keystore.                                                                                                                                                             |
| `atoti.server.endpoint.mcp.oauth2.authserver.jwk.key-alias`         | string       | *unset*                 | Alias of the signing key entry in the keystore.                                                                                                                                        |
| `atoti.server.endpoint.mcp.oauth2.authserver.client.client-id`      | string       | `atoti-mcp`             | Pre-registered client ID. MCP clients normally self-register via Dynamic Client Registration and do not need this.                                                                     |
| `atoti.server.endpoint.mcp.oauth2.authserver.client.redirect-uris`  | list         | loopback defaults       | Allowed OAuth 2.0 redirect URIs.                                                                                                                                                       |
| `atoti.server.endpoint.mcp.oauth2.authserver.client.scopes`         | list         | `[mcp.read, mcp.write]` | Scopes the pre-registered client may request.                                                                                                                                          |
| `atoti.server.endpoint.mcp.oauth2.authserver.consent.enabled`       | boolean      | `true`                  | Show the consent page ("Allow Atoti to connect to \<client>?") before issuing a code. When `false`, a code is issued immediately after authentication, with no consent step.           |

## Limitations and security notes

**Open Dynamic Client Registration.** The `/oauth2/register` endpoint accepts unauthenticated
registration requests by design: any caller can register a client, but no token is issued until a
user completes interactive login at `/oauth2/authorize`. For deployments with strict registration
policies, place the endpoint behind rate-limiting or a registration filter.

**In-memory client store.** Dynamically registered clients are held in memory. They are lost on
server restart, but clients re-register transparently on the next connection attempt.

**Ephemeral signing key.** If no RSA keypair is configured (neither `atoti.jwt.key.*` nor a
keystore), an ephemeral key is generated at startup. All tokens issued with that key become invalid
on restart. A warning is logged. Use a persistent key in production.

**Token lifetime.** Access tokens default to one hour. Configure
`atoti.server.endpoint.mcp.oauth2.authserver.access-token-ttl` to a shorter value in
security-sensitive environments.

## When to use each OAuth 2.1 mode

| Scenario                                                                                      | Recommended mode                                                                                                          |
| --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| The deployment uses a corporate IdP (Okta, Entra ID, Auth0, Keycloak) and SSO/MFA is required | `external` (see [Configure OAuth 2.1 discovery](./configure-oauth2-discovery))                                            |
| Atoti already manages user authentication and no external IdP is available or desired         | `self-issued` (this page)                                                                                                 |
| Prototyping or local development without any IdP                                              | `self-issued` with the application providing a user store (the `apps/basic` sample application includes an in-memory one) |

## Related reading

* [Atoti MCP server setup guide](./atoti-mcp-server-setup)
* [Configure OAuth 2.1 discovery](./configure-oauth2-discovery) (`external` mode with a
  corporate IdP)
* [Connect with Claude](./connect-with-claude)
* [RFC 8414: OAuth 2.0 Authorization Server Metadata](https://datatracker.ietf.org/doc/html/rfc8414)
* [RFC 7591: OAuth 2.0 Dynamic Client Registration Protocol](https://datatracker.ietf.org/doc/html/rfc7591)
* [RFC 9728: OAuth 2.0 Protected Resource Metadata](https://datatracker.ietf.org/doc/html/rfc9728)
* [MCP authorization spec (2025-06-18)](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization)
