Worked Example: Minting and Validating a JWT for KG_AUTH_REQUIRED Mode¶
What this demonstrates. CONCEPT:OS-5.14, authenticated identity
enforcement: with KG_AUTH_REQUIRED=1 the gateway mints the request's
ActorContext (actor / roles / tenant) server-side from a validated JWT —
caller-supplied _actor/_roles/_tenant kwargs are ignored — and requests
without a valid Bearer token are rejected 401, fail-closed, with only health
probes and /metrics exempt. You mint a token, call the gateway with it, and
see every failure mode. Deep dives:
Autonomous governance and zero trust,
Configuration.
Prerequisites (ladder rung). The secured rung of
Deployment configurations: the REST gateway
(python -m agent_utilities) plus an identity provider that serves a JWKS endpoint
(Keycloak, Azure AD, Okta, Auth0 — anything OIDC). For a self-contained demo,
any static HTTP server hosting a JWKS JSON works; that is exactly what the
smoke run below did.
1. Server configuration¶
The relevant typed flags (agent_utilities/core/config.py):
# .env on the gateway
KG_AUTH_REQUIRED=1 # enforce server-validated identity (default 0)
AUTH_JWT_JWKS_URI=https://idp.example.test/realms/agents/protocol/openid-connect/certs
AUTH_JWT_ISSUER=https://idp.example.test/realms/agents # optional but recommended
AUTH_JWT_AUDIENCE=graph-os # optional but recommended
# stdio MCP servers have no Authorization header; their process identity comes
# from a validated token instead (only consulted when KG_AUTH_REQUIRED is on):
# KG_AUTH_TOKEN=<jwt>
Facts to know, verified against agent_utilities/security/auth.py and
agent_utilities/security/request_identity.py:
- Validation is JWKS-based (
AUTH_JWT_JWKS_URI, fetched over HTTP and cached for 5 minutes). There is no shared-secret (HS256) server option on this path — keys come from the JWKS document, and thejoserfclibrary (theauthextra) verifies the signature. - Accepted claims:
exp/iatare validated with 30s leeway;issmust equalAUTH_JWT_ISSUERandaudmust equal/containAUTH_JWT_AUDIENCEwhen those are configured. - The claims-to-actor mapping (
actor_from_claims, first match wins): actor_id←sub|client_id|azproles←roles|realm_access.roles(Keycloak) | space-separatedscope/scptenant_id←tenant_id|tenant|org_id|tidactor_type← HUMAN when anemailclaim is present, else AUTOMATED_SERVICE (provenance only)- the minted actor carries
authenticated=True - An invalid token is always 401 — even when
KG_AUTH_REQUIRED=0. - With
KG_AUTH_REQUIRED=1, requests with no token are 401 except the exempt paths:/health,/healthz,/api/health,/api/healthzand/metrics(scrapers cannot mint JWTs;/metricscarries only aggregate counters). - With
KG_AUTH_REQUIRED=0(the default), unauthenticated requests pass through with a one-time prominent startup warning — the legacy honor-system mode.
2. Mint a token and call the gateway (Python)¶
The token below was minted with PyJWT and validated through the tree's real
code path (actor_from_bearer_token) in the smoke run:
import time
import httpx
import jwt # pyjwt
PRIVATE_KEY_PEM = open("demo_rsa_private.pem").read() # pair of the JWKS key
now = int(time.time())
token = jwt.encode(
{
"sub": "agent:harvest-runner",
"iss": "https://idp.example.test/realms/agents", # must match AUTH_JWT_ISSUER
"aud": "graph-os", # must match AUTH_JWT_AUDIENCE
"iat": now,
"exp": now + 3600,
"roles": ["kg.writer", "workflow.executor"],
"tenant_id": "acme",
},
PRIVATE_KEY_PEM,
algorithm="RS256",
headers={"kid": "demo-key-1"}, # kid should match a key in the JWKS
)
resp = httpx.post(
"http://localhost:9000/api/graph/query",
headers={"Authorization": f"Bearer {token}"},
json={"cypher": "MATCH (n) RETURN count(n) AS c"},
timeout=30,
)
print(resp.status_code, resp.json())
(In production the IdP mints the token — client-credentials grant for service actors — and you never hold the private key yourself.)
Expected actor minted server-side from this token (captured from the smoke run):
{"actor_id": "agent:harvest-runner",
"roles": ["kg.writer", "workflow.executor"],
"tenant_id": "acme",
"authenticated": True,
"actor_type": "automated_service"}
Every KG read/write in the request is scoped to that actor: ontology
permissioning rows, audit attribution, and (with KG_BRAIN_ENFORCE on) the
fail-closed ACL gate all see it. Under KG_AUTH_REQUIRED=1, any
_actor/_roles/_tenant tool kwargs a caller supplies are ignored entirely.
3. The same call with curl¶
TOKEN="eyJhbGciOiJSUzI1NiIsImtpZCI6ImRlbW8ta2V5LTEi..." # from your IdP # sanitizer:ignore
curl -s http://localhost:9000/api/graph/query \
-H "Authorization: Bearer $TOKEN" \
-H 'content-type: application/json' \
-d '{"cypher": "MATCH (n) RETURN count(n) AS c"}'
4. Failure modes¶
All captured by driving ActorIdentityMiddleware directly in the smoke run
(KG_AUTH_REQUIRED=1, JWKS configured):
| Request | Status | Body |
|---|---|---|
No Authorization header |
401 |
{"error": "Authentication required (KG_AUTH_REQUIRED=1): provide a valid JWT Bearer token"} |
| Malformed/forged token | 401 |
{"error": "Invalid token signature"} |
| Expired token | 401 |
{"error": "Token has expired"} |
Wrong iss/aud |
401 |
{"error": "Invalid token claim: ..."} |
No token, GET /health |
200 |
(exempt) |
No token, GET /metrics |
200 |
(exempt) |
| Valid token | 200 |
(request proceeds as the minted actor) |
KG_AUTH_REQUIRED=1 but AUTH_JWT_JWKS_URI unset, no token |
401 |
{"error": "Authentication required (KG_AUTH_REQUIRED=1): provide a valid JWT Bearer token (server misconfigured: AUTH_JWT_JWKS_URI unset)"} |
All 401 responses carry a WWW-Authenticate: Bearer header. Note that this
identity layer answers in 401 terms (who are you); resource-level denials
for an authenticated actor surface from the ontology permissioning gate as
PermissionError inside the tool/endpoint result (e.g. the ORCH-1.42 workflow
permission gate — see the
ontology-to-workflow example), not as an HTTP 403
from this middleware.
Stdio MCP servers: with KG_AUTH_REQUIRED=1 and no valid KG_AUTH_TOKEN, the
process identity falls back to a restricted read-only system actor — write
tools are gated until a validated token identity exists.
Verification: smoke-run against this tree (2026-06-11). Executed:
python3 -m pytest tests/unit/core/test_request_identity.py -q (passed, 38
combined with the evolution-bridge suite), plus a live one-off that generated
an RSA keypair, served its JWKS from a local HTTP server, minted the exact
token above with PyJWT 2.10.1, validated it through the real
actor_from_bearer_token path (actor dict above captured verbatim), and drove
ActorIdentityMiddleware for the no-token / bad-token / health / metrics /
valid-token rows of the failure table (statuses and bodies captured verbatim).
The expired-token and wrong-claim rows are from _decode_jwt's explicit
error branches in agent_utilities/security/auth.py, exercised by the unit
suite rather than the one-off.