Skip to content

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.

Managed Agent ── POST /mcp ── Authorization: Bearer <jwt> ──► mesh7
                                                           validate JWT
                                                           extract agent ID
                                                           apply policies