Centralized Auth
The Centralized Auth use case describes enforcing auth once, at the gateway, instead of per upstream. This tutorial runs a gateway with two toolkits, billing and support, both behind a single jwt guard on north_mcp_server, and shows that one guard configuration protects both.
Prerequisites
docker-compose.yaml
services:
gateway:
image: ghcr.io/aklivity/zilla:latest
pull_policy: always
depends_on:
- api
ports:
- 7114:7114
volumes:
- ./zilla.yaml:/etc/zilla/zilla.yaml
command: start -v -e
api:
image: kennethreitz/httpbin
ports:
- 8000:80zilla.yaml
name: centralized-auth
guards:
agent_jwt:
type: jwt
options:
issuer: https://auth.example.com
audience: https://mcp.example.com
keys:
- kty: RSA
kid: example-key
alg: RS256
n: s9UPTz1zecQcug_nFPhAw0J8FcyiHFKirFRYJOkYq90pKEm_lgDhf9Acyt5qSh-2vjldpuPR_wEa91K2Nzu-3VhXEFuRZtn_6sCLcSxSEVpSZkZR_u7V3CM-9EX6iGzvLs0CXb4VXVwrxuVf8G5T6dEdzqTqRkArjYEbbQgkZoFbb_NBWc13rP9nJ1suH1_Cghohb-U7usIH08jVbznOhVwTp1I8dcZpvXGHejRSpePq9pus1aJ-LYS9EYTJjC_eMJWARZj1dx51_pAGfPlNS0o9s8KAFMQm-O8QbxPP_X2ekg8umzPrTLdflFdwnpBGxVXNmtbEc5CBlTQr0rKx4Q
e: AQAB
stores:
cache:
type: memory
catalogs:
api_catalog:
type: inline
options:
subjects:
billing_params:
schema: |
{
"type": "object",
"properties": { "id": { "type": "string" } },
"required": [ "id" ]
}
support_params:
schema: |
{
"type": "object",
"properties": { "id": { "type": "string" } },
"required": [ "id" ]
}
bindings:
north_tcp_server:
type: tcp
kind: server
options:
host: 0.0.0.0
port: 7114
routes:
- when:
- port: 7114
exit: north_http_server
north_http_server:
type: http
kind: server
options:
access-control:
policy: cross-origin
routes:
- when:
- headers:
":path": /mcp
exit: north_mcp_server
#region authorization
north_mcp_server:
type: mcp
kind: server
options:
authorization:
agent_jwt:
credentials: "Bearer {credentials}"
exit: north_mcp_proxy
#endregion authorization
north_mcp_proxy:
type: mcp
kind: proxy
options:
cache:
store: cache
ttl: PT5M
routes:
- exit: billing_http_proxy
when:
- toolkit: billing
- exit: support_http_proxy
when:
- toolkit: support
billing_http_proxy:
type: mcp-http
kind: proxy
options:
tools:
get_payment_status:
description: Look up the status of a payment by identifier.
summary: "Routed to ${result.url}"
schemas:
input:
model: json
catalog:
api_catalog:
- subject: billing_params
version: latest
routes:
- when:
- tool: get_payment_status
exit: sys:http_client
with:
headers:
":method": GET
":scheme": http
":authority": api:80
":path": /anything/billing/${args.id}
support_http_proxy:
type: mcp-http
kind: proxy
options:
tools:
get_ticket_status:
description: Look up the status of a support ticket by identifier.
summary: "Routed to ${result.url}"
schemas:
input:
model: json
catalog:
api_catalog:
- subject: support_params
version: latest
routes:
- when:
- tool: get_ticket_status
exit: sys:http_client
with:
headers:
":method": GET
":scheme": http
":authority": api:80
":path": /anything/support/${args.id}The guard sits on north_mcp_server, in front of both toolkit routes on north_mcp_proxy, not duplicated onto billing_http_proxy and support_http_proxy individually:
north_mcp_server:
type: mcp
kind: server
options:
authorization:
agent_jwt:
credentials: "Bearer {credentials}"
exit: north_mcp_proxyNote
The keys above is a real RSA public key with no known private key, useful for the rejection checks below. To also verify a successful, authenticated call, swap in your own identity provider's issuer/audience (or its own keys) and present a token it issued.
Start the Stack
docker compose up -dVerify the Guard Protects Both Toolkits
A malformed token is rejected before it reaches either toolkit:
curl -i http://localhost:7114/mcp -H 'Authorization: Bearer not-a-real-token'HTTP/1.1 401 UnauthorizedA request with no token at all is admitted as unauthorized rather than rejected outright, the same behavior an unguarded route would show:
curl -i http://localhost:7114/mcpHTTP/1.1 200 OKNeither toolkit has its own auth check: billing_http_proxy and support_http_proxy are unaware auth exists at all. Adding a third toolkit later means one more route on north_mcp_proxy, not a third guard.
Stop the Stack
docker compose downNext Steps
- See JWT Bearer and the
jwtguard reference for using a real identity provider'sissuer/audienceand remote JWKS instead of manually configuredkeys. - See Secure Agent Access for the token exchange grant, swapping the agent's one credential for a different downstream credential per upstream.

