Federated enforcement.
OpenID AuthZEN enforcement points for Kong, PingAccess, Envoy, Istio, agentgateway and Node. Each one puts an AI agent's request to a Policy Decision Point before it goes through - and learns which PDP from what the resource publishes, or from a federation whose word outranks it. When the answer is no, the agent gets a challenge it can resolve, not a 403.
- Enforces at
- Kong, PingAccess, Envoy, Istio, agentgateway, and inside a Node service
- Speaks
- OpenID AuthZEN 1.0, to any conformant PDP
- Authorises
- REST calls and MCP tool calls, per call
- Finds the PDP
- from the resource's published metadata, or the federation's word
- On a deny
- a challenge the agent can resolve, in the header, the body and the MCP error
- When a PDP is down
- closed by default, open only where you say, and never quietly
An agent is not a session
An AI agent calling a tool is one agent, acting for one human, touching one resource, right now. Whether that is allowed depends on all three, and it belongs in policy, not in tool code. AuthZEN gives an enforcement point a standard way to put that question to a decision point. These are the enforcement points.
It acts under delegation
The token an agent carries names two parties: the agent, and the customer it acts for. A gateway that only checks "is this token valid" cannot tell whether this agent may do this for this customer. The policy has to see the delegation, not just the bearer.
The decision depends on the request
A balance enquiry and a payment over ten thousand dollars arrive on the same connection with the same token. One is fine on a password; the other needs the customer's approval, or a verified credential. That is a policy decision, made per request.
A flat 403 stops the agent dead
A human reads an error page. An agent needs to know what would make the request succeed: which scope to obtain, which credential to present, whether anyone is logged in at all. A deny that says how to resolve it is the difference between an agent that completes the task and one that gives up.
The gateway reads who decides. It does not decide.
Most enforcement points are told where their PDP is and stay pointed there. These read it per resource: from the resource's own metadata, or, when the resource belongs to an OpenID Federation, from a trust chain that runs back to an anchor you installed by hand. Then they hand the PDP the resource's own requirements and let the PDP do the matching. The gateway compares nothing.
The federation's word
OpenID Federation 1.0 trust chainThe resource's metadata as it survives every superior's policy, signed back to a trust anchor you configured. The federation operator can pin which PDPs may decide for a resource, put an estate PDP in front of every member, and set an authentication floor no member can lower by editing its own document.
The resource's own document
RFC 9728 protected resource metadataWhat the resource publishes about itself: which PDP decides for it, the layers in front, the scopes it uses, the acr it expects. Good enough inside one trust domain, forwarded to the PDP verbatim and labelled as self-asserted.
The PDP you configured
a static URLWhere a resource that publishes nothing lands, and the whole of today's behaviour if you turn discovery off. The static PDP is the only one that ever receives the configured API key.
- The federation's word wins. When a trust anchor is configured, the gateway resolves the resource's chain and never reads the resource's own document.
- An invalid chain fails closed. A resource that claims membership and cannot prove it gets a 503, never a fall-back to its own word or to the static PDP.
- Allowlists on every call. A discovered PDP or metadata URL outside the allowlist is refused, whoever named it, and a refusal never falls through to a weaker source.
- The PEP forwards, the PDP matches. The resource's requirements reach the PDP as a document, with the endpoint hit and the token. Policy lives in one place, and the deny stays resolvable because the PDP's reason names the requirement and where it read it.
- A metadata outage is not an authorisation outage. Metadata is cached with stale-while-failing, refresh throttling and negative caching, so a down resource does not put a fetch in every request's path.
- No guessed paths. Endpoints come from the PDP's own AuthZEN metadata; a batch is only sent to a PDP that advertises a batch endpoint.
Layered policy
A route names an ordered list of PDPs, or the resource publishes one in authzen_policy_layers, or the federation adds its PDP to every member's. An estate-wide PDP that judges the token and the client goes first; the resource's own PDP after it. Every layer must permit. A layer may be marked fail-open, for the advisory PDP that is not worth an outage, and a permit that skipped one says so in a response header. A deny never opens.
# a route's layers: the estate PDP, then whatever the resource's document names
pdp_layers: "https://estate.example fail-open, resource"
The gateway is the resource's federation face
A federated resource publishes two things: a signed entity configuration the trust controller onboards, and its RFC 9728 metadata. The gateway is the resource's public face, so it publishes both. The split is deliberate: the gateway holds a key, the controller holds the policy. What the federation resolves is what the gateway republishes, signed, so a plain RFC 9728 consumer reads the federation's word without knowing a federation is behind it.
The demo shows the flip: a gateway's document goes from self-asserted to the controller's word when the anchor onboards it, and nothing on the gateway changes.
One request, whichever edge it reaches
- The agent calls
A REST endpoint or an MCP tool, with a delegated token: the customer is the subject, the agent is the actor.
- The enforcement point reads it
Validates the token and the DPoP proof, reads who is acting for whom, and maps the request, or the tool call's arguments, into an AuthZEN evaluation.
- It finds the PDP
From what the resource publishes, or what the federation resolved for it, and asks each layer of policy in turn.
- The PDP decides
On the delegation, the request, the resource's own requirements and whatever risk signals it holds. Permit, deny, or deny with advice.
- The agent goes on, or resolves
A permit is proxied upstream with the delegation recorded in headers. A deny comes back as a challenge: step up, prove identity, or log in.
A deny the agent can act on
Three challenge types, the same three words from every enforcement point, in the HTTP header, the JSON body and the MCP error. An agent resolves them without a human reading prose.
resource_authorisationThe customer approves this scope at the authorisation server, an RFC 9470 step-up. The agent retries with the scope.
identity_proofingThe customer presents a verified credential from their wallet, an mDL. The policy that asked for it is satisfied on the retry.
authnThere is no authenticated user yet. login_required, with the acr to log in at.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer
error="insufficient_scope",
scope="payments:approve"
{ "error": "insufficient_scope",
"scope": "payments:approve",
"reason": "payments over 1000 need the
customer's approval" }
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer
error="identity_verification_required",
doctype="org.iso.18013.5.1.mDL"
{ "error": "identity_verification_required",
"doctype": "org.iso.18013.5.1.mDL",
"reason": "no verified identity on file
for account origination" }
HTTP/1.1 200 OK
{ "jsonrpc": "2.0", "id": 7,
"error": {
"code": -32001,
"message":
"insufficient_scope scope=payments:approve",
"data": { "authz_challenge": {
"type": "resource_authorisation",
"scope": "payments:approve",
"pep": "mcp-edge" } } } }
data member for one that parses.Deploy where your traffic already is
Each enforcement point takes the same per-route knobs, spelled the same way: REST or MCP style, whether a token is required, whether it must be DPoP-bound, which resource is guarded, which layers of policy to ask. Configure it in whichever gateway's own terms, and read every knob in the configuration reference.
Kong Gateway
A native plugin on a route or service, DB-less or with a database. Request mapping, discovery and challenges in Lua; DPoP and MCP tool calls delegated to the engine.
luarocks install kong-plugin-authzen-pdp-0.4.0-1.all.rock
PingAccess
An Add-on SDK rule on an application's API policy, configured in the PingAccess console. Reads the identity PingAccess validated, and verifies the customer's token itself. Drops into deploy/; no PingAccess upgrade.
cp authzen-pdp-pingaccess-<version>.jar $PA_HOME/deploy/
Envoy, Istio, agentgateway
No plugin to install. The gateway attaches its external authorisation filter to the engine, and per-route settings travel as context extensions. One binary serves all three; the agentgateway and Istio attachments are in the tree.
docker pull ghcr.io/id-partners/coaz-pep:0.4.0
Your own Node service
For a service that is its own enforcement point: Express middleware for REST and a guard for MCP tool calls, with the same client, verdict and challenge shapes. No runtime dependencies. An unmatched route is a deny.
npm install @id-partners/authzen-pep
Kong, in front of PingAuthorize's Sideband API
The same discovery, the same layers and the same failure rules, where the policy owns the request mapping and the deny. For an estate that already speaks Sideband and wants the federation's word on who decides.
luarocks install kong-plugin-sideband-pdp-0.4.0-1.all.rock
See all of it run
A stub trust anchor, resources that are and are not members, two banks' PDPs, an estate PDP and a rogue one that permits everything. A console runs one request through three discovery modes and shows what each read, forwarded and decided. Profiles add Kong and PingAccess in front of the same resource.
Open the demo
Everything with a specification behind it, implemented once
coaz-pep is one Go binary with two front doors: an Envoy ext_authz gRPC service and an HTTP check API. It carries the parts a gateway plugin cannot safely reimplement, so there is one evaluation and one challenge, not four that drift. The Kong plugin and the PingAccess rule call it for what needs a JOSE library; the Node SDK can delegate to it.
Authorises the tool call, not the endpoint
For MCP the request is one HTTP POST. The engine reads the JSON-RPC body, finds the tool, and authorises that call under the COAZ framework and its MCP binding: the MCP server's tool list declares how a call's arguments and the token's claims become an AuthZEN question. A mapping cannot name a different subject than the token carries. Every method is decided; an unknown one is denied.
Delegation in the decision
The agent is the subject, the customer it acts for travels with it, and the context carries the customer token's scope, authentication level, any rich authorisation details and the audience. The policy reasons about the delegation, not just the bearer.
Token and sender-constraint verification
Access and user tokens verified against your JWKS, with issuer and audience checks. DPoP proofs (RFC 9449) verified, so a stolen token is useless without the key. Every surface delegates this to the one implementation.
Refuses to run insecurely by accident
It will not start without its check-API token, its upstream allowlist, and the JWKS, issuer and audience to verify tokens against, plus the discovery allowlists when discovery is on, and it names every missing one at once. PEP_ALLOW_INSECURE=true is the hatch, logged every time, for development and demos only.
# the image, amd64 and arm64, signed with cosign, with an SBOM and build provenance
docker run --rm -p 9191:9191 -p 9192:9192 \
-e AUTHZEN_URL=https://pdp.bank.example/tenants/bank-a -e AUTHZEN_API_KEY=… \
-e CHECK_API_TOKEN=… -e MCP_UPSTREAM_ALLOWLIST=https://mcp.bank.example \
-e ACCESS_TOKEN_JWKS_URL=… -e ACCESS_TOKEN_ISSUER=… -e ACCESS_TOKEN_AUDIENCE=… \
ghcr.io/id-partners/coaz-pep:0.4.0
Every environment variable, the per-route knobs, readiness, the SIGTERM drain, audit logs and metrics: the coaz-pep reference.
From the demo to your gateway
- See it
The hosted demo, or
cd demo && docker compose up --build -d && ./demo.sh. Press Discover; pickmember; watch the federation column deny what the resource column believed. - Pick a surface
The gateway your traffic already flows through, or the SDK where a service must enforce for itself. Each one's install and every knob is in the reference.
- Give it a PDP
Any AuthZEN 1.0 PDP. Ours put that face on PingAuthorize: a Go proxy, or in-process as a Server SDK servlet. Start static, with
AUTHZEN_URL. - Turn discovery on
Publish the resource's RFC 9728 document with
authzen_policy_decision_points, or join a federation and let the anchor say. Set the allowlists; nothing off them is ever asked.
The architecture
What is in the repository, how the pieces fit, and the decisions behind them. The map.
one pageThe overview
Nobody tells the gateway where the PDP is: the two discoveries, who gets to say who decides, and what it buys you. For showing rather than reading.
docs/referenceThe configuration reference
One page per component, every configurable item with its type, default and meaning, a complete configuration for each, and the screens where there are screens.
demo/README.mdThe demo, explained
The cast, the levers, the scripted walkthrough and the console, and the two arguments the demo keeps apart: is it safe, and is it worth turning on.
CHANGELOG.mdWhat changed
Every component shares one version line. A v* tag releases them all: the image, both Kong rocks, the PingAccess jar, the npm package. Upgrade notes lead each release.
Reporting a vulnerability
How to report one privately, and what to expect. Every suite runs offline; CI runs govulncheck and npm audit, and a coverage ratchet that only ever moves up.
Built on standards, and one parameter of our own
Everything here has a specification behind it, except the parameter that names a resource's PDPs in RFC 9728 metadata: authzen_policy_decision_points is this project's, a single constant in each codebase, provisional pending a working-group profile. authzen_policy_layers is its companion.
- OpenID AuthZEN 1.0 the evaluation contract, and the PDP metadata at
/.well-known/authzen-configuration - COAZ the AuthZEN framework and its MCP binding: tool-call authorisation, per call
- RFC 9728 protected resource metadata,
scopes_supported,signed_metadata - OpenID Federation 1.0 entity configurations, subordinate statements, trust chains, metadata policy
- RFC 9470 step-up authentication challenges
- RFC 9449 DPoP, sender-constrained tokens
- RFC 8707 resource indicators: the identifier discovery starts from
- RFC 8693 · RFC 9396 token exchange and rich authorisation requests, the delegation the policy sees
- Envoy ext_authz the gRPC attachment for Envoy, Istio and agentgateway
- PingAccess Add-on SDK the rule, and the console form it draws
Built and run in the open by ID Partners
ID Partners is an Australian digital identity consultancy. We built this, we run it in the open, and we work with the Ping products underneath it every week. We also contribute to the OpenID AuthZEN working group, where the profile that authorises tool calls is being written. The engagement fits the stage you are at.
Architecture and policy workshop
- Your agentic use cases mapped to enforcement points, decision points and the tokens between them
- Which gateway carries which traffic, and where a service must enforce for itself
- The delegation model: token exchange, the actor claim, step-up and consent, identity proofing
One use case, end to end, in your environment
- The enforcement point on your gateway, or the SDK in your service, in non-production
- Your API's metadata published, discovery on, the PDP reached through the AuthZEN adapter
- An agent that resolves the challenges, demonstrated to your stakeholders
Production hardening and support
- Allowlists, DPoP, user-token verification, federation trust anchors, fail mode per layer
- Rollout across applications and the other gateways in the estate, one contract throughout
- A named engineer, and the fix in the open-source line rather than a fork you carry
