
How do we build interoperable authorization for AI agents?
OpenID AuthZEN standardizes the API between Policy Enforcement Points (PEPs) and Policy Decision Points (PDPs), enabling authorization decisions to be made independently of the enforcement layer and the policy engine implementation.
AI agents increasingly act on behalf of users by calling external tools through the Model Context Protocol (MCP), an open protocol that enables AI agents to discover and invoke tools and resources at runtime. Every tool call is an operation that needs an authorization decision.
In this article, I apply this approach to MCP tool authorization:
- Keycloak provides identity, authentication, and delegation as the Identity Provider (IdP), and evaluates authorization policies as the Policy Decision Point (PDP).
- Agent Gateway acts as the Policy Enforcement Point (PEP).
- OpenID AuthZEN standardizes the interaction between them.
Agent Gateway translates each MCP tools/call into a standardized AuthZEN evaluation request to Keycloak, then enforces the decision. Different users receive different permissions based on externally managed policies, without hardcoding authorization rules in the gateway. This brings Zero Trust principles (never trust, always verify) to agentic execution, at the moment a tool is actually invoked.
While this lab demonstrates authorization at the gateway layer, the same approach extends across the stack, with enforcement at gateways, APIs, and applications. Open standards let us connect IdPs, gateways, policy engines, and other components like Lego building blocks, creating a composable architecture that evolves with our needs without locking us into a single implementation.
This is a hands-on companion to Mastering Secure APIs and AI Agents with Zero Trust and Fine-Grained Authorization Based on OpenID AuthZEN. The implementation is available as a standalone lab in my agentic-security-labs repository, and was contributed upstream in agentgateway/agentgateway#3478, which adds the
examples/mcp-authzenexample to the Agent Gateway project.
Table of contents
Open Table of contents
The problem
With MCP, agents discover and invoke tools at runtime, across multiple servers, on behalf of users. That raises the classic IAM questions with new urgency: who is calling (the user, the agent, or both), which tools can they invoke, and where is that decision enforced?
Too often, the answer is authorization logic hardcoded inside each MCP server and API, in its own proprietary form. That doesn’t scale: every component speaks its own authorization language, rules drift across services, and every policy change requires a code change and a new deployment. The issue is not where authorization is enforced, but that the policy logic is tightly coupled to each implementation. The prior article covers these problems in depth.
Why AuthZEN?
If you’re new to OpenID AuthZEN, the simplest way to think about it is:
AuthZEN standardizes how an application asks an external authorization service whether an operation is allowed.
OpenID AuthZEN addresses this by standardizing a common interface between two components:
- Policy Enforcement Point (PEP) the component receiving the request and enforcing the authorization decision.
- Policy Decision Point (PDP) the component that evaluates the request against authorization policy and returns the decision.
In this implementation, Agent Gateway is the PEP and Keycloak is the PDP.
Instead of embedding authorization logic in every application, you externalize the decision to a dedicated policy engine. Agent Gateway does not need to understand how the authorization policy is implemented. It only needs to construct an AuthZEN authorization request and enforce the resulting decision.
What does an AuthZEN request look like?
AuthZEN defines a standard request model based on four main concepts:
{
"subject": {
"type": "user",
"id": "alice",
"properties": {}
},
"resource": {
"type": "tool",
"id": "approve_payment",
"properties": {}
},
"action": {
"name": "tools/call",
"properties": {}
},
"context": {}
}
In simple terms:
subject: who is requesting the operation.resource: what the subject wants to access.action: what operation they want to perform.context: additional information that may influence the decision.
The PDP evaluates that request against its policies and replies with a Decision Response: {"decision": true} or {"decision": false}.
The PEP does not need to know whether the PDP uses roles, attributes, relationships, or another authorization model internally. That policy logic stays outside the application enforcing the request.
Core benefits
- Interoperability: connect any AuthZEN-compliant PDP without changing the authorization interface in the enforcing application.
- Decoupled authorization: policy decisions are separated from enforcement logic.
- Flexibility: the same interface can support different authorization models, including ReBAC, PBAC, and ABAC.
- Policy independence: the PEP does not need to know how authorization policies are implemented.
- Dynamic evaluation: decisions can use runtime context rather than relying only on permissions established when a token was issued.
Why is this useful for MCP?
MCP introduces a dynamic tool invocation model. An agent can discover tools and invoke them at runtime, so authentication alone does not answer an important question:
Is this caller allowed to invoke this particular tool right now?
AuthZEN fills that gap as a dynamic, externalized authorization layer alongside OAuth. For protected tool calls, the authorization decision is evaluated at request time against the current policy rather than relying solely on permissions established when the session was created.
This article stays focused on MCP, but nothing about the PEP/PDP contract is MCP-specific. The same evaluation request shape (subject, resource, action, context) works for a plain REST API, which is the point: one standard, one PDP integration, enforced consistently across APIs, MCP servers, and whatever else sits in the request path. The prior article covers the API and MCP Apps cases in more depth.
The same architecture can later be extended to fine-grained authorization, where the decision also depends on the arguments of the tool call, such as the payment amount or destination account.
AuthZEN complements OAuth
OAuth is an access delegation standard that enables an application (such as a service or an agent) to securely obtain temporary permission to access a user’s resources without the user sharing their credentials. After successful authentication, the Identity Provider issues a temporary access token to the application, allowing it to act on the user’s behalf within a limited scope.
The MCP authorization specification builds on OAuth 2.1. In this lab, the simplified OAuth role mapping is:
- Keycloak → OAuth Authorization Server.
- MCP client → OAuth Client.
- Payments MCP server, protected by Agent Gateway → OAuth Resource Server (the
mcp_proxyaudience).
If these roles aren’t familiar, Aaron Parecki’s OAuth for Model Context Protocol is a great introduction.
AuthZEN doesn’t replace OAuth; it complements it.
- OAuth answers who is calling, and is the token valid for this resource? In this lab, Keycloak issues the access token, and the gateway’s
jwtAuthpolicy validates it against the realm’s issuer, JWKS, and themcp_proxyaudience before any authorization decision is made. - AuthZEN answers can this subject perform this operation, right now? For every
tools/call, the PDP evaluates the request against the current policy, and afalsedecision is rejected with403.
OAuth scopes are set when the token is issued and typically operate at a broad level. A scope like payments:write would not distinguish create_payment from approve_payment, and a change in permissions is only reflected once a new token is issued. A per-tool decision evaluated at request time addresses both.

The diagram, from the prior article, shows the MCP server acting as the PEP. In this lab, Agent Gateway performs these two steps in front of the MCP server, enforcing coarse-grained authorization at the tool level. This does not remove the need for authorization at the MCP server: fine-grained decisions that depend on tool arguments or resource data (which account, which payment, what amount) still belong closer to the resource, at the MCP server or the API behind it, which can also act as AuthZEN PEPs.
Architecture
The example has four components:
| Component | Role |
|---|---|
| MCP client | Calls the Payments MCP tools |
| Agentgateway | MCP gateway and AuthZEN Policy Enforcement Point |
| Keycloak | OpenID Provider and AuthZEN Policy Decision Point |
| Payments MCP server | Backend exposing the payment tools |
Agent Gateway sits between the MCP client and the Payments MCP server. For a tools/call request, it validates the caller’s token, constructs an AuthZEN evaluation request, sends it to Keycloak, and enforces the resulting decision.
Keycloak plays two roles in this example:
- It issues the OAuth access token used by the MCP client.
- It evaluates the AuthZEN authorization request as the PDP.
The resulting flow is:
The token exchange step is Keycloak-specific plumbing. It authenticates the gateway to Keycloak’s protected AuthZEN evaluation endpoint. It is not part of the AuthZEN protocol itself.
A different AuthZEN-compliant PDP could use a different authentication mechanism without changing the basic PEP/PDP model.
OAuth setup
The Keycloak mcp realm defines two OAuth clients.
mcp-client is used by the MCP client to obtain the user’s access token for the MCP session. An audience mapper adds aud=mcp_proxy, so the token passes the gateway’s jwtAuth audience check.
mcp_proxy represents the MCP resource server, so it is the audience the gateway expects. The gateway also authenticates as mcp_proxy to perform the token exchange and call Keycloak’s AuthZEN evaluation endpoint. Its authorization settings hold the tool resources, the tools/call scope, and the policies the PDP evaluates.
Note: to keep the lab self-contained,
cli-tests.shobtains tokens with the Resource Owner Password Credentials grant. Don’t do this in production. Real MCP clients should use the Authorization Code flow with PKCE, as required by the MCP authorization specification.
The Agentgateway config, block by block
Everything below comes from the working example in agentic-security-labs/mcp-authzen, also available upstream as examples/mcp-authzen.
This is the authorization-relevant part of the example’s config.yaml, walked through step by step.
1. Validate the inbound session token
mcp:
port: 3000
policies:
jwtAuth:
issuer: http://localhost:7080/realms/mcp
audiences: [mcp_proxy]
jwks:
url: http://localhost:7080/realms/mcp/protocol/openid-connect/certs
Standard JWT validation against Keycloak’s mcp realm: this validates the caller’s token and establishes the identity and claims used by the authorization flow.
2. Only invoke the PDP for tool calls
extAuthz:
conditional:
- condition: has(mcp.tool)
host: localhost:7080
mcp.tool is only populated on a tools/call request. initialize, tools/list, and other MCP requests never invoke the PDP. The gate is a single CEL expression, so there’s no additional authorization overhead for MCP operations that don’t require an authorization decision.
3. Authenticate the gateway to the AuthZEN endpoint (Keycloak-specific)
policies:
backendAuth:
oauthTokenExchange:
host: localhost:7080
path: /realms/mcp/protocol/openid-connect/token
subjectToken:
source:
header:
name: x-token-exchange-subject
audiences: [mcp_proxy]
clientAuth:
clientId: mcp_proxy
clientSecret: mcp-proxy-secret
method: clientSecretBasic
This is the RFC 8693 token exchange from the sequence diagram: the gateway trades the caller’s validated token for one issued to mcp_proxy, which is what’s actually authorized to call Keycloak’s AuthZEN evaluation endpoint. A PDP that exposes its evaluation endpoint differently wouldn’t need this block.
4. Build the AuthZEN evaluation request
protocol:
http:
path: '"/realms/mcp/authzen/access/v1/evaluation"'
addRequestHeaders:
content-type: '"application/json"'
:method: '"POST"'
x-token-exchange-subject: jwt.rawToken.unredacted()
body: |
{
"subject": {"type": "user", "id": jwt.sub},
"resource": {"type": "tool", "id": mcp.tool.name},
"action": {"name": "tools/call"},
"context": {"agent": jwt.azp}
}
metadata:
decision: json(response.body).decision
This is where the MCP request becomes an AuthZEN authorization request:
| AuthZEN field | Source | Example |
|---|---|---|
subject.id | jwt.sub | alice |
resource.id | mcp.tool.name | approve_payment |
action.name | fixed value | tools/call |
context.agent | jwt.azp | mcp-client |
This mapping follows a general model of AuthZEN CoAZ MCP binding.
In this example, authorization is intentionally tool-level to keep the implementation simple. The same approach can be extended to fine-grained authorization (FGA) by including tool arguments or other context in the authorization request.
5. Enforce the decision
authorization:
rules:
- require: '!has(mcp.tool) || extauthz.decision == true'
One line: if this isn’t a tool call, let it through; if it is, require decision == true. Everything upstream exists to make this expression answerable.
Running it
You can run the example in either of two ways.
Option A: Standalone lab (agentic-security-labs)
The quickest way to run it is the standalone lab in embesozzi/agentic-security-labs:
git clone https://github.com/embesozzi/agentic-security-labs.git
cd agentic-security-labs/mcp-authzen
docker compose -f docker-compose.yaml up -d
Option B: Agent Gateway repository
If you prefer to build Agent Gateway from source, run it from the root of agentgateway/agentgateway:
# Start Keycloak, bootstrap the mcp realm, start the Payments MCP server
docker compose -f examples/mcp-authzen/docker-compose.yaml up -d
# Start agentgateway with the example config
cargo run -- -f examples/mcp-authzen/config.yaml
The keycloak/bootstrap-authzen.sh script provisions the mcp realm, including the users, roles, OAuth clients, and authorization resources and policies used by the example.
How the Keycloak policies work
The policies live in Keycloak’s Authorization Services, configured on the mcp_proxy client. The bootstrap script creates four kinds of objects:
- Resources: one per MCP tool, of type
tool, named after the tool (list_accounts,get_account,create_payment,approve_payment,cancel_payment). - Scope: a single
tools/callscope, attached to every tool resource. - Policies: one role policy per role.
payments-user-policymatches users withpayments_user,payments-manager-policymatchespayments_manager, andpayments-admin-policymatchespayments_admin. - Permissions: one per tool. Each permission states which policies may perform
tools/callon that tool. Access is granted if any of the listed policies matches.
The AuthZEN request fields line up with these objects: resource.id identifies the tool resource, action.name identifies the tools/call scope, and subject.id identifies the user whose roles are checked against the permission’s policies.
For example, the approve_payment permission lists only payments-manager-policy and payments-admin-policy. When Alice calls approve_payment, she only holds payments_user, so no listed policy matches and Keycloak returns {"decision": false}. Bob holds payments_manager, so payments-manager-policy matches and Keycloak returns {"decision": true}.
The mock Payments MCP server exposes five tools (list_accounts, get_account, create_payment, approve_payment, cancel_payment), and access varies by role:
| Role | list_accounts | get_account | create_payment | approve_payment | cancel_payment |
|---|---|---|---|---|---|
payments_user | ✓ | ✓ | ✓ | ✗ | ✗ |
payments_manager | ✓ | ✓ | ✓ | ✓ | ✓ |
payments_admin | ✓ | ✓ | ✓ | ✓ | ✓ |
Three users are configured: alice (payments_user), bob (payments_manager), and carol (payments_admin).
Seeing it work
This example includes cli-tests.sh which drives an MCP tool call end to end. It authenticates a user, establishes an MCP session, and invokes the requested tool.
These are the test cases covered below:
| User | Role | Tool | Expected |
|---|---|---|---|
| alice | payments_user | list_accounts | 200 allowed |
| alice | payments_user | create_payment | 200 allowed |
| alice | payments_user | approve_payment | 403 denied by the PDP |
| bob | payments_manager | approve_payment | 200 allowed |
| carol | payments_admin | cancel_payment | 200 allowed |
Alice (payments_user)
Alice can list and create a payment, but she cannot approve one:
./cli-tests.sh alice list_accounts # 200 - allowed
./cli-tests.sh alice create_payment # 200 - allowed, creates pay-1
./cli-tests.sh alice approve_payment # 403 - denied
Alice can create a payment because that’s within her role. approve_payment requires payments_manager, which she doesn’t hold, so agentgateway sends Keycloak this evaluation request for the approve_payment attempt:
{
"subject": {"type": "user", "id": "alice"},
"resource": {"type": "tool", "id": "approve_payment"},
"action": {"name": "tools/call"},
"context": {"agent": "mcp-client"}
}
and gets back {"decision": false}. Agentgateway’s authorization rule rejects the call before it ever reaches the Payments MCP server.
Bob (payments_manager)
Bob has everything Alice has, plus approval authority.
./cli-tests.sh bob approve_payment # 200 - allowed, approves pay-1
The evaluation request agentgateway sends is identical in shape, just with bob’s subject:
{
"subject": {"type": "user", "id": "bob"},
"resource": {"type": "tool", "id": "approve_payment"},
"action": {"name": "tools/call"},
"context": {"agent": "mcp-client"}
}
This time Keycloak returns {"decision": true}, and the call goes through. Same code path, same config, same extAuthz call; the only thing that changed is the AuthZEN decision Keycloak returned.
Carol (payments_admin)
The admin role isn’t automatically broader than manager. On these five tools, the PDP grants Carol exactly the same access as Bob, because the policy is defined per tool and role pair rather than through a role hierarchy:
./cli-tests.sh carol cancel_payment '{"payment_id":"pay-1"}' # 200 - allowed
A few things worth noticing in these runs:
- The
403for alice never reaches the payments server. It’s rejected by agentgateway’sauthorizationrule directly from Keycloak’sdecision: false, before the request is proxied anywhere. - Every tool call, allowed or denied, goes through the identical authorization path. Nothing tool-specific is hardcoded in Agent Gateway’s config; the only thing that varies is the PDP’s answer.
initializeandtools/listalways succeed regardless of role, sinceextAuthzonly fires whenmcp.toolis set, i.e., only ontools/call.
Where this goes next: from CGA to FGA
What’s implemented here is coarse-grained authorization (CGA) at the tool level: is this subject allowed to call this tool? This keeps the example focused on the core PEP/PDP integration.
The same architecture can also support fine-grained authorization (FGA), where the decision depends on the specific arguments of the call, such as the payment amount, account, or recipient, rather than just the tool name. I’ll cover these extensions in future articles and workshops as the OpenID AuthZEN Working Group’s CoAZ MCP binding draft matures.
Another possible extension is to filter tools/list results through the PDP, so each caller only discovers the tools it is allowed to call. In this lab, initialize and tools/list do not call the PDP.
The gateway is also only one enforcement point. Enterprise deployments typically place PEPs at multiple trust boundaries: coarse-grained authorization at the MCP gateway, and fine-grained, resource-level decisions at the MCP server and the APIs behind it, each acting as an AuthZEN PEP:

Because every PEP uses the same AuthZEN request and response contract, each layer can enforce decisions appropriate to its level of detail without introducing a different authorization interface per component.
Beyond FGA, authorization can also consider the user’s intent and the agent’s planned actions, rather than evaluating each tool call in isolation. I explore this model in Intent Agent Native Authorization, where authorization is tied to the intent behind an agentic workflow and can be combined with explicit user approval for sensitive actions.
Conclusion
This example demonstrates how OpenID AuthZEN can be used to externalize authorization for MCP tool calls.
Agent Gateway acts as the Policy Enforcement Point, Keycloak acts as the Policy Decision Point, and AuthZEN provides the standardized interface between them.
The current implementation performs coarse-grained authorization at the tool level:
Can this subject call this MCP tool?
It deliberately does not yet make decisions based on tool arguments.
That provides a simple foundation for extending the same architecture toward fine-grained authorization, where the PDP can evaluate the specific operation being requested.
The key idea is simple:
Agent Gateway enforces the decision. Keycloak makes the decision. AuthZEN connects them.
You can try the example from the standalone lab in embesozzi/agentic-security-labs or from the Agent Gateway examples, where it was merged upstream in agentgateway/agentgateway#3478.