# OAuth with the helper library

`diagrid.identity` hands your agent — or any Catalyst-fronted ASGI service — the verified identity of the user who invoked it. It ships as ASGI middleware rather than a framework plugin, so it works with any Python agent framework you serve over ASGI. For where this fits in the wider flow, see the [Enterprise identity overview](https://docs.diagrid.io/develop/agents/enterprise-identity).

The library is **Python-only**. Other SDKs are not yet available.

## The header contract

The caller presents its upstream IdP token in `X-Diagrid-User-Token`. Catalyst verifies that token, exchanges it for a **Catalyst-issued** user token, and overwrites the same header before the request reaches your agent:

```
X-Diagrid-User-Token: Bearer <catalyst-user-jwt>
```

So the header your agent reads always carries the post-exchange Catalyst token — your code never touches the raw upstream credential. The middleware reads this header, validates the Catalyst token, and gives your handler a `VerifiedUser`.

## Install

`diagrid.identity` ships inside the `diagrid` package. Install the `identity` extra, which brings the JWT and ASGI dependencies the middleware needs:

```bash
pip install "diagrid[identity]"
```

Add it alongside your agent framework's extra when you need both, for example `pip install "diagrid[langgraph,identity]"`.

Core types (`OAuthConfig` and `VerifiedUser`) live in `diagrid.identity`; the middleware and the accessor that reads the verified caller live in `diagrid.identity.asgi`:

```python
from diagrid.identity import OAuthConfig, VerifiedUser
from diagrid.identity.asgi import OAuthMiddleware, verified_user
```

## Configure

`OAuthConfig` is the identity policy the middleware enforces on every request. Construct it once at startup:

```python
from diagrid.identity import OAuthConfig

oauth = OAuthConfig(scopes={"agent.invoke"})
```

| Field | Default | Description |
|---|---|---|
| `scopes` | none | Scopes every caller must present. The middleware rejects a verified token that is missing any of them with `403`. |
| `require_auth` | `true` | Whether a request without `X-Diagrid-User-Token` is rejected with `401`. Set it to `false` when unauthenticated routes, such as health and readiness probes, share the same app. On an unauthenticated request `verified_user(request)` returns `None`, so check the result before using it. A token that *is* present is always verified, and an invalid one always rejected, either way. |
| `issuer` | discovered | Expected `iss` claim. |
| `audience` | discovered | Expected `aud` claim. |
| `jwks_uri` | discovered | Endpoint the middleware fetches signing keys from. |

The last three are discovered from your sidecar when the middleware handles its first authenticated request, not at startup, so leave them unset unless you are running somewhere the sidecar is not reachable and need to point the middleware at the issuer yourself.

## Attach the middleware

`OAuthMiddleware` is ASGI middleware, so it works with FastAPI, Starlette, or any other ASGI app. It verifies the token on every request, rejects unauthenticated calls before they reach your handlers, and makes the `VerifiedUser` available to the ones it admits. Read it with `verified_user(request)`:

```python
from fastapi import FastAPI, Request
from diagrid.identity import OAuthConfig, VerifiedUser
from diagrid.identity.asgi import OAuthMiddleware, verified_user

app = FastAPI()
oauth = OAuthConfig(scopes={"agent.invoke"})
app.add_middleware(OAuthMiddleware, config=oauth)

@app.post("/invoke")
async def invoke(request: Request):
    user: VerifiedUser = verified_user(request)
    return {"subject": user.subject}
```

With the default `require_auth=True`, a request that reaches your handler has already been verified, so the result is never `None` and you can use it directly. Set `require_auth=False` and you have to check.

Your handler reads the caller rather than the credential: there is no token to parse, and no header to trust or distrust. The middleware never touches `request.state.user`, so an app that sets that one itself keeps its own object.

The middleware also keeps the caller's token available for the lifetime of the request. That is what carries the user's identity onto the MCP tools and sub-agents your handler calls — see [On behalf of](https://docs.diagrid.io/develop/agents/enterprise-identity/on-behalf-of).

## What `VerifiedUser` gives you

A `VerifiedUser` is the trusted, decoded identity of the caller. Use it to make authorization decisions or to attribute actions to a user.

| Field | Description |
|---|---|
| `subject` | The user's stable subject identifier (`sub` claim). |
| `tenant` | The tenant the user belongs to. |
| `scopes` | The scopes granted to the user. |
| `claims` | The full set of decoded token claims. |
| `issuer_id` | The identifier of the federation that issued and validated the token. |

## Rejection semantics

The middleware rejects a request with a JSON body carrying a machine-readable code, under one of three statuses:

- **`401 Unauthorized`** — the caller failed authentication. The token is missing, malformed, expired, or its signature, issuer, or audience does not validate.
- **`403 Forbidden`** — the caller authenticated successfully but is not authorized. The verified user lacks a scope your policy requires.
- **`503 Service Unavailable`** — the middleware could not verify the token at all, because it has no issuer to validate against or has not yet loaded the signing keys. This is retryable.

See [Troubleshooting](https://docs.diagrid.io/develop/agents/enterprise-identity/troubleshooting) for every code and the debugging path for each.

## Reference implementation

The module is open source at [`diagridio/python-ai`](https://github.com/diagridio/python-ai/tree/main/diagrid/identity). Read it for the exact behavior this page describes.

## What's next

- [On behalf of](https://docs.diagrid.io/develop/agents/enterprise-identity/on-behalf-of) — propagate this identity to downstream MCP tools automatically.
- [Troubleshooting](https://docs.diagrid.io/develop/agents/enterprise-identity/troubleshooting) — resolve rejection codes.
