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.
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:
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:
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:
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):
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.
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 for every code and the debugging path for each.
Reference implementation
The module is open source at diagridio/python-ai. Read it for the exact behavior this page describes.
What's next
- On behalf of — propagate this identity to downstream MCP tools automatically.
- Troubleshooting — resolve rejection codes.