> For the complete documentation index, see [llms.txt](https://api-docs.realfinity.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://api-docs.realfinity.io/mcp/connecting/authentication.md).

# Authentication

Every call to the Realfinity MCP server's JSON-RPC endpoint is authenticated. `POST /mcp` requires an OAuth 2.1 bearer token; there is no API-key mode and no anonymous mode. Only `GET /health` is unauthenticated, and it exists solely as a liveness probe.

Endpoints:

| Environment | MCP endpoint                       |
| ----------- | ---------------------------------- |
| Production  | `https://ai.realfinity.io/mcp`     |
| UAT         | `https://ai-uat.realfinity.io/mcp` |

All of the OAuth metadata, `/authorize`, `/token`, and `/health` paths below are served from the same host as the `/mcp` endpoint you are connecting to. The examples on this page use production.

## The surprising part: the server is its own authorization server

Read this section before you write any client code, because it explains a design that looks wrong until you know why it is there.

The server's real identity provider is Auth0. Access tokens are Auth0-issued, signed by Auth0, and validated against the Auth0 audience `https://api.realfinity.io/mcp-api`. Under RFC 9728 the correct discovery chain would be: a client reads the protected resource's metadata, finds the upstream `authorization_servers` entry pointing at Auth0, and runs the authorization code flow directly against Auth0.

{% hint style="info" %}
**claude.ai's custom-connector flow treats the MCP server itself as the authorization server rather than following the RFC 9728 chain to the upstream issuer.** The OAuth facade at `/.well-known/oauth-authorization-server`, `/authorize`, and `/token` exists to satisfy that client.
{% endhint %}

So the server advertises *itself* as both the protected resource and the authorization server, then does the work the client would otherwise have skipped: `/authorize` overrides the audience regardless of what the client sends and redirects to Auth0, and `/token` proxies the code exchange to Auth0's token endpoint. The tokens your client ends up holding are ordinary Auth0 tokens — the facade shapes the *handshake*, not the credential.

```mermaid
sequenceDiagram
    participant C as MCP client
    participant M as MCP server (ai.realfinity.io)
    participant A as Auth0
    C->>M: GET /.well-known/oauth-protected-resource
    M-->>C: authorization_servers = [ itself ]
    C->>M: GET /.well-known/oauth-authorization-server
    M-->>C: /authorize + /token on itself, S256, code + refresh_token
    C->>M: GET /authorize with PKCE challenge
    Note over M: strips audience + resource, re-adds MCP audience, force-adds offline_access
    M-->>C: 302 to Auth0 /authorize
    C->>A: user logs in and consents
    A-->>C: redirect back with code
    C->>M: POST /token (code + verifier)
    M->>A: POST /oauth/token (proxied)
    A-->>M: access_token + refresh_token
    M-->>C: access_token + refresh_token
    C->>M: POST /mcp with Bearer access_token
    Note over M: validate against Auth0, then resolve the Realfinity agent
```

## Discovery endpoints

Both metadata documents are public and unauthenticated. Their contents are derived from the request host, so the UAT host returns the same shape with UAT URLs.

### Protected resource metadata

```bash
curl -s https://ai.realfinity.io/.well-known/oauth-protected-resource
```

```json
{"resource":"https://ai.realfinity.io","authorization_servers":["https://ai.realfinity.io"],"bearer_methods_supported":["header"],"scopes_supported":[]}
```

Note that `authorization_servers` points back at the MCP server, not at Auth0 — that is the facade, as described above. `bearer_methods_supported` is `header` only: send the token as `Authorization: Bearer <token>`. `scopes_supported` is deliberately empty; authorization is enforced by Realfinity role, not by OAuth scope.

### Authorization server metadata

```bash
curl -s https://ai.realfinity.io/.well-known/oauth-authorization-server
```

```json
{"issuer":"https://ai.realfinity.io","authorization_endpoint":"https://ai.realfinity.io/authorize","token_endpoint":"https://ai.realfinity.io/token","response_types_supported":["code"],"grant_types_supported":["authorization_code","refresh_token"],"code_challenge_methods_supported":["S256"],"token_endpoint_auth_methods_supported":["client_secret_post","client_secret_basic"]}
```

In plain terms:

| Capability                                  | Value                                                              |
| ------------------------------------------- | ------------------------------------------------------------------ |
| Response types                              | `code` — authorization code flow only; no implicit, no device code |
| Grant types                                 | `authorization_code`, `refresh_token`                              |
| PKCE                                        | `S256` required; `plain` is not offered                            |
| Client authentication at the token endpoint | `client_secret_post` or `client_secret_basic`                      |

Note also what the metadata document does *not* contain: there is no `registration_endpoint` key. Under RFC 8414 that omission is the authoritative statement that the server offers no dynamic client registration, and you can confirm it in one curl by reading the JSON above. In practice this means **clients must be pre-registered in Auth0 — there is no self-service registration.** Realfinity issues your client credentials out of band; if your OAuth library expects to register itself via RFC 7591, configure the credentials manually instead.

## `GET /authorize` rewrites your request

This is the second thing integrators get wrong, and it is worth being precise about. `/authorize` does not simply forward your query string to Auth0. It rewrites the request:

1. It **removes** any `audience` parameter you supplied.
2. It **removes** any `resource` parameter you supplied.
3. It **re-adds** `audience=https://api.realfinity.io/mcp-api` — the Auth0 API identifier for the MCP server.
4. It **adds** `offline_access` to your `scope` value if it isn't already there.

Everything else — `client_id`, `redirect_uri`, `response_type`, `state`, `code_challenge`, `code_challenge_method` — is passed through untouched, and the response is a `302` to Auth0's `/authorize`.

{% hint style="warning" %}
**Do not set `audience` or `resource` yourself.** Whatever you send in those parameters is discarded, so a client that relies on them will not get the token it expects. If your OAuth library insists on sending a `resource` parameter for RFC 8707 resource indicators, that is fine — it is dropped harmlessly — but do not build any logic on the assumption that it reaches Auth0.
{% endhint %}

The `offline_access` injection is deliberate: without a refresh token, the connection dies the moment the access token expires and the user has to reconnect by hand. With it, the client can silently renew through the `refresh_token` grant. Your client does not need to request `offline_access` — but it does need to *store* the refresh token it gets back and use it.

Do not hard-code an assumed access token lifetime. The only guarantee the flow makes is that a refresh token is available, so treat a `401` on `/mcp` as "refresh and retry once" rather than scheduling renewal against a fixed expiry.

## `POST /token`

`POST /token` reads your request body and forwards it verbatim to Auth0's `/oauth/token`, preserving the original content type (`application/x-www-form-urlencoded` by default) and forwarding the `Authorization` header if you sent one — which is how `client_secret_basic` works through the proxy. Auth0's status code, content type, and body are returned to you unchanged.

Practically, this means you send a standard authorization-code or refresh-token grant and read a standard Auth0 token response. Both grants go to the same URL:

```bash
# authorization_code exchange
curl -s -X POST https://ai.realfinity.io/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=authorization_code' \
  -d 'client_id=<your client id>' \
  -d 'client_secret=<your client secret>' \
  -d 'code=<code from the redirect>' \
  -d 'code_verifier=<your PKCE verifier>' \
  -d 'redirect_uri=<your registered redirect uri>'
```

```bash
# refresh
curl -s -X POST https://ai.realfinity.io/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=refresh_token' \
  -d 'client_id=<your client id>' \
  -d 'client_secret=<your client secret>' \
  -d 'refresh_token=<stored refresh token>'
```

## What a validated token becomes

Bearer validation is only the first half of authentication. Once the Auth0 signature, issuer, audience, and lifetime check out, the server runs an `OnTokenValidated` step that turns the token into a *Realfinity* identity:

1. It reads the **email claim** from the token.
2. It looks up the Realfinity agent for that email via `IAgentService.GetByEmail`. **If no agent matches, the request fails.**
3. It loads that agent's role codes via `IAgentService.GetRoles`.
4. It attaches a new identity carrying `https://claims.realfinity.io/user_id` (the Realfinity user id) plus one role claim per role code, and a baseline `User` role.

{% hint style="danger" %}
**A valid Auth0 login is not enough.** If the email on the token has no matching Realfinity agent record, authentication fails and *every* tool call returns 401 — even though the login succeeded, consent was granted, and the token is genuinely valid. This is the single most common cause of "it connected but nothing works."

The fix is not in your client: the person authenticating must sign in with the email address of an existing Realfinity agent.
{% endhint %}

The `https://claims.realfinity.io/user_id` claim is the mechanism behind user-scoped results. Tools do not accept a caller identity as a parameter — they read it from this claim. That is why the same tool call returns different loans, pipelines, and tasks for different users, and why you cannot query another agent's data by changing an argument.

### Roles and tool authorization

Role claims are what tool-level authorization is checked against. The default role set required by a tool is:

```
Admin,Processor,DualLicensedAgent,ConciergeKey,Agent
```

A tool that mirrors a Realfinity Private API endpoint uses that endpoint's own role list instead, so a few tools are narrower than the default. Roles also shape *results*, not just access — for example, Admin sees all loans, ConciergeKey sees managed agents, and everyone else sees loans they originate or referred.

A **Processor** is a special case worth stating precisely: for per-loan access — `get_loan_application`, its contacts and task tools, and `search_loan_applications` — a Processor reaches **all loans org-wide**, exactly like an Admin, and is *not* limited to their own company. Only the aggregate and reporting tools narrow a Processor to their own company's users. The company-bounded branch in the loan authorization path belongs to the separate **CompanyAdmin** role. Narrower still: `get_user_licenses` and the pricing tools apply a per-user access check with no Processor branch at all.

If a token authenticates but a specific tool returns an authorization error while others work, the cause is a role the agent does not hold — not a token problem.

## The 401 challenge

An unauthenticated `POST /mcp` returns `401` with two `WWW-Authenticate` headers:

```bash
curl -s -D - -o /dev/null -X POST \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' \
  https://ai.realfinity.io/mcp
```

```http
HTTP/1.1 401 Unauthorized
Content-Length: 0
Server: Kestrel
WWW-Authenticate: Bearer resource_metadata="https://ai.realfinity.io/.well-known/oauth-protected-resource"
WWW-Authenticate: Bearer
```

The first header is the useful one: the `resource_metadata` parameter points a spec-compliant client at the protected resource metadata document so it can discover where to authenticate. The second, bare `Bearer` header is emitted by the underlying framework challenge. Clients should read the header carrying `resource_metadata` and ignore the bare one.

A `401` on `/mcp` therefore means one of three things: no token, an invalid or expired token, or — per the callout above — a valid token whose email has no matching Realfinity agent. The response body is empty in all three cases, so use `GET /health` to separate "the server is down" from "my credentials are wrong":

```bash
curl -s https://ai.realfinity.io/health
```

## Internal-only second bearer scheme

The server registers a second bearer scheme alongside the Auth0 one and routes requests between them by inspecting the JWT's issuer. This is why an unauthenticated request to `/mcp` returns two `WWW-Authenticate` headers rather than one.

That second scheme is internal: there is no endpoint that will issue you a token for it, so integrate through the Auth0 flow described above. Both schemes run the same `OnTokenValidated` agent-resolution step, so a token from either is scoped to exactly the same agent identity and roles — nothing about tool behavior or scoping differs between them.
