Skip to content

JWT Authentication

Since v0.13.0, flux7-mesh validates JWT tokens from external identity providers. The IdP issues the tokens, and mesh7 enforces them.

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
allow_legacy No false Accept the plaintext Bearer agent:<name> form alongside JWTs (migration only)

No auth.jwt block = no validation. The legacy Bearer agent:<name> format still works, for backward compatibility. With auth.jwt set, it is rejected with 401 unless allow_legacy: true.

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>" → allow_legacy on: name used as agent ID (no validation)
  │                          allow_legacy off: HTTP 401
  │
  └─ "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, and the same policies apply.

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

Stdio clients under auto-proxy

When a daemon is running, mesh7 --mcp does not serve tools itself: it becomes a stdio→HTTP shuttle to the daemon's POST /mcp. By default the shuttle identifies itself with the legacy Bearer agent:<--mcp-agent> header, which a JWT-strict daemon rejects. Give the client a token instead:

{
  "mcpServers": {
    "mesh7": {
      "command": "mesh7",
      "args": ["--mcp", "--config", "config.yaml"],
      "env": { "MESH_AGENT_TOKEN": "<jwt issued for this agent>" }
    }
  }
}

With MESH_AGENT_TOKEN set, the shuttle sends Authorization: Bearer <token> and the daemon derives the agent (and user) from its claims; --mcp-agent is then ignored for identity. The token is read once at start, so a short-lived token needs the client restarted when it expires.