# Authentication

MCP traffic on Catalyst crosses two authentication boundaries, and Catalyst handles each one separately:

1. **Caller → Catalyst** — the agent authenticates to Catalyst's [MCP proxy endpoint](https://docs.diagrid.io/develop/mcp/connect) with its API token.
2. **Catalyst → upstream MCP server** — Catalyst authenticates to the real server using credentials configured on the [`MCPServer`](https://docs.diagrid.io/operate/project-operations/mcp-servers) connection.

The agent only ever holds its own Catalyst token. The upstream server's credentials live on the connection and in your project's secret store — never in agent code, prompts, or logs.

## Caller to Catalyst

Every request to `<DAPR_HTTP_ENDPOINT>/v1.0/diagrid/mcp/<mcp-server-name>` must carry the calling agent's API token in the `dapr-api-token` header:

```python
headers = {"dapr-api-token": DAPR_API_TOKEN}
```

`diagrid dev run` injects `DAPR_API_TOKEN` for each app during local development; for a hosted project, read the agent's token from the [Catalyst console](https://catalyst.diagrid.io). Catalyst validates the token, then applies the [access policy](https://docs.diagrid.io/develop/mcp/mcp-access-policies) for the target server before forwarding the call. This is the same identity Catalyst uses for every other call the agent makes.

## Catalyst to the upstream server

Configure the upstream credential once on the `MCPServer` connection. Catalyst attaches it to each forwarded request and strips the caller's `dapr-api-token` so it never leaks upstream. Catalyst supports three authentication shapes, listed strongest first.

### SPIFFE JWT (recommended)

SPIFFE JWT is the strongest way to authenticate Catalyst to your MCP server, and the recommended choice whenever your server supports it: it is **secretless** and provides **true zero trust** authentication. No credential is ever provisioned or stored — Catalyst presents a short-lived, audience-scoped JWT-SVID minted from its own workload identity and rotated automatically, and your server verifies it against Catalyst's OIDC issuer.

This approach is supported by **any OIDC-capable server**: the server validates the token against the issuer's discovery URL, confirms the expected audience, and authorizes the subject (`sub`). You control each of these values, so the same standard OIDC verification a server already performs is sufficient to authenticate Catalyst.

Configure `spec.endpoint.streamableHTTP.auth.spiffe.jwt` with the header to carry the token and the expected audience:

```yaml
spec:
  endpoint:
    streamableHTTP:
      url: https://mcp.example.com/mcp
      auth:
        spiffe:
          jwt:
            header: Authorization
            headerValuePrefix: "Bearer "
            audience: https://mcp.example.com
```

#### Verify the SPIFFE JWT on your server

Catalyst acts as the identity provider here: the calling sidecar attaches a short-lived JWT-SVID for the `MCPServer`'s backing ID to requests it forwards to your URL. No secret is provisioned — the credential is a short-lived, audience-scoped, signed assertion of the caller's workload identity, minted from Catalyst's own workload identity and rotated automatically. Your server's job is to **validate** it. The token is a standard OIDC-discoverable RS256 JWT, so any JWT library works — you need no SPIFFE-specific tooling. Treat a request as authenticated from Catalyst only when **all** of these hold:

| Claim | What to require | Value |
|---|---|---|
| signature | Verified against the issuer's JWKS | RS256, keys from the issuer's `jwks_uri` |
| `iss` | Equals your region's Catalyst issuer | e.g. `https://oidc.r1.diagrid.io` — see below |
| `aud` | **Contains** the audience you configured | `[ <your audience>, <region trust domain> ]` |
| `exp` / `nbf` / `iat` | Token is currently valid | short-lived; rotated automatically |
| `sub` | Identifies the calling workload | `spiffe://<region-trust-domain>/ns/prj-<project-id>/<app-id>` |

**Find the issuer, then discover the signing keys.** The `iss` is your **region's** OIDC issuer — the data-plane value, not the control-plane API host. Read it from the region status:

```bash
diagrid region get <region> -o yaml
# status.endpoints.oidc: https://oidc.r1.diagrid.io
```

Point your JWT library at that issuer's discovery document rather than hardcoding a key URL:

```
https://oidc.r1.diagrid.io/.well-known/openid-configuration
```

It returns the `jwks_uri` (`https://oidc.r1.diagrid.io/jwks.json`) and the signing algorithm (`RS256`). Most JWT libraries take the issuer, perform this discovery, then fetch, cache, and rotate the keys for you — Catalyst rotates its signing keys, so do not pin a single key. The value is also always present as the `iss` claim on a token your server receives.

**The audience is the value you control end-to-end.** Whatever you set in `auth.spiffe.jwt.audience` is stamped into the token's `aud` — alongside the region trust domain, so `aud` is multi-valued. Choose a string that names your server (`mcp://payments`, `https://mcp.example.com`) and require that your value is **present** in `aud`. This is what stops a token minted for a different server from being replayed against yours; most JWT libraries do this membership check when you pass an expected audience.

**Pinning `sub` is recommended hardening.** The `sub` is the SPIFFE ID of the identity backing the `MCPServer` (same name as the connection), in the form `spiffe://<region-trust-domain>/ns/prj-<project-id>/<app-id>`. Read the exact value with:

```bash
diagrid app get <mcpserver-name> --project <project> -o yaml
# status.spiffeId: spiffe://<cluster>.<region>.global.public.diagrid.io/ns/prj-<project-id>/<app-id>
```

Verifying the signature, `iss`, `aud`, and expiry establishes authenticity; validating `sub` restricts *which* Catalyst identity you accept.

You don't have to pin an exact `sub`. Because the SPIFFE ID is hierarchical, your server can glob-match it to accept a whole project or a set of apps by naming convention:

- Every app in a project — `spiffe://<region-trust-domain>/ns/prj-<project-id>/*`
- Apps sharing a prefix — `spiffe://<region-trust-domain>/ns/prj-<project-id>/mcp-server-*`

This matching is your server's own authorization logic against the `sub` claim, not Catalyst configuration — your server matches the claim against a glob pattern rather than an exact string.

The token arrives in the header you named in the spec (`Authorization` above), with any `headerValuePrefix` (`Bearer `) stripped before parsing.

### OAuth 2.0 client credentials

For servers that issue tokens through the OAuth 2.0 client-credentials grant, configure the issuer and client on `spec.endpoint.streamableHTTP.auth.oauth2`. Catalyst acquires and refreshes the token and attaches it to each call.

```yaml
spec:
  endpoint:
    streamableHTTP:
      url: https://mcp.example.com/mcp
      auth:
        oauth2:
          issuer: https://auth.example.com
          audience: https://mcp.example.com
          clientID: my-client-id
          scopes: ["mcp:read", "mcp:write"]
          clientSecret: my-client-secret
```

### Static headers

The most common shape — an API key or bearer token sent as a request header. Provide the full header value, including any `Bearer ` prefix:

```yaml
apiVersion: dapr.io/v1alpha1
kind: MCPServer
metadata:
  name: my-mcp
spec:
  endpoint:
    streamableHTTP:
      url: https://mcp.example.com/mcp
      headers:
        - name: Authorization
          value: "Bearer <token>"
```

When you provide a credential — through `diagrid mcpserver create --header`, the interactive prompt, the console, or a plaintext `value` in a spec — Catalyst extracts it into your project's [secret store](https://docs.diagrid.io/operate/project-operations/secrets) automatically; it never lands in the control-plane database as plaintext.

The built-in [catalog entries](https://docs.diagrid.io/references/mcp-servers) come with the right shape pre-filled for each provider — header-based for most, OAuth 2.0 for Pendo and Snowflake, SPIFFE for Cloudflare — so you only supply the credential.

## What's next

- [Control tool access](https://docs.diagrid.io/develop/mcp/mcp-access-policies) — layer per-tool authorization on top of authentication.
- [Add an MCP server](https://docs.diagrid.io/develop/mcp/mcpserver-getting-started) — set the upstream credential when you register a connection.
- [Security and trust posture](https://docs.diagrid.io/concepts/mcp/mcp-security-posture) — the full identity and trust model.
