JWT Authentication
Since v0.13.0, flux7-mesh validates JWT tokens from external identity providers. mesh7 enforces — the IdP issues tokens.
How it works
External IdP (Auth0, Cloudflare Access, Keycloak)
│
└── issues JWT with agent identity in claims
│
▼
Agent ── Authorization: Bearer <jwt> ──► mesh7
│
1. Fetch JWKS (cached, refresh 5 min)
2. Validate signature (RS256 / ES256)
3. Check exp, iss, aud
4. Extract agent ID from claim
│
▼ policy engine (same YAML rules)
mesh7 does not issue tokens. It validates them against the IdP's public keys (JWKS endpoint) and extracts the agent identity from a configurable claim.
Configuration
auth:
jwt:
jwks_url: https://auth.example.com/.well-known/jwks.json
issuer: https://auth.example.com
audience: mesh7
agent_claim: sub
| Field | Required | Default | Description |
|---|---|---|---|
jwks_url |
Yes | — | JWKS endpoint URL (public keys) |
issuer |
No | — | Validate iss claim if set |
audience |
No | — | Validate aud claim if set |
agent_claim |
No | sub |
Which claim is used as agent ID for policies |
user_claim |
No | (off) | Which claim names the human the agent acts for. Empty means tokens carry no user and traces record none |
No auth.jwt block = no validation. The legacy Bearer agent:<name> format still works, backward compatible.
Delegation: the agent and the user
A token can carry two identities at once — the agent making the call, and the
human it acts for. With user_claim set, every trace entry records user_id
and the OTel export carries enduser.id, so a call answers on whose behalf
it happened, not only by which agent.
The user is optional by design: a client-credentials token (an agent acting for no one) has no user, and that absence is a fact the policy may act on, never a validation error.
The session on the MCP Streamable HTTP transport is bound to the full identity (agent and user) on every request: a session ID is not a credential, and cannot be reused by another agent or across users.
Data plane only
JWT governs agent identity on the data plane. Operator endpoints (traces, grants, approvals, policies) are guarded separately — see Control Plane Auth.
Auth flow
Authorization header present?
│
├─ No → anonymous
│
├─ "Bearer agent:<name>" → legacy path (no validation, name used as agent ID)
│
└─ "Bearer <jwt>" → validate against JWKS
├─ Valid → extract agent_claim → agent ID for policies
└─ Invalid → HTTP 401 Unauthorized
Key distinction: 401 (not authenticated — bad token) vs 403 (authenticated but policy denied).
JWKS caching
mesh7 fetches the JWKS at startup and caches it in memory. A background goroutine refreshes the cache every 5 minutes. No per-request fetch.
If the initial JWKS fetch fails, mesh7 does not start — fail-closed.
Supported algorithms
- RSA — RS256, RS384, RS512
- ECDSA — ES256 (P-256), ES384 (P-384), ES512 (P-521)
Tokens must include a kid header matching a key in the JWKS response.
Example: per-agent policies with JWT
auth:
jwt:
jwks_url: https://auth.example.com/.well-known/jwks.json
issuer: https://auth.example.com
agent_claim: sub
policies:
- name: admin-agents
agent: "admin-*"
rules:
- tools: ["*"]
action: allow
- name: worker-agents
agent: "worker-*"
rules:
- tools: ["filesystem.read_*"]
action: allow
- tools: ["filesystem.write_*"]
action: human_approval
- tools: ["*"]
action: deny
An agent with JWT claim sub: admin-bot matches the first policy. An agent with sub: worker-3 matches the second.
IdP examples
Cloudflare Access
auth:
jwt:
jwks_url: https://<team>.cloudflareaccess.com/cdn-cgi/access/certs
issuer: https://<team>.cloudflareaccess.com
audience: <application-aud-tag>
agent_claim: email
Auth0
auth:
jwt:
jwks_url: https://<tenant>.auth0.com/.well-known/jwks.json
issuer: https://<tenant>.auth0.com/
audience: https://mesh.example.com
Keycloak
auth:
jwt:
jwks_url: https://<host>/realms/<realm>/protocol/openid-connect/certs
issuer: https://<host>/realms/<realm>
audience: mesh7
agent_claim: azp # the client — the agent
user_claim: sub # the subject — the human it acts for
Keycloak puts the client id in azp and the user in sub, which is the
delegation pair. Keycloak also documents the OAuth identity-chaining draft and
its use as an MCP authorization server, so the same realm can issue the token
an API gateway (Kong, ...) validates before handing the call to mesh7.
Local development
For testing without a real IdP, use the generate-token.py script in examples/jwt-auth/ :
pip install cryptography PyJWT
# Start a local JWKS server
python generate-token.py serve &
# Generate a token
TOKEN=$(python generate-token.py token --sub my-agent)
# Test
curl -H "Authorization: Bearer $TOKEN" http://localhost:9090/health
See examples/jwt-auth/ for the full setup.
MCP Streamable HTTP
JWT validation also applies to MCP Streamable HTTP connections (POST /mcp). Managed Agents or remote MCP clients presenting a valid JWT are identified by their claim value — same policies apply.