An Apache APISIX plugin that implements the AuthZEN authorization API standard, enabling standardized policy-based access control through any AuthZEN-compliant Policy Decision Point (PDP).
This development transforms Apache APISIX into an AuthZEN-compliant PEP, externalizing authorization decisions to any AuthZEN-compatible PDP. This approach:
- Secures AI/MCP tool calls - Authorize Model Context Protocol (MCP) requests with fine-grained access control
- Centralizes decision-making - Easier to manage and update policies across all applications
- Decouples authorization from code - Improves security, flexibility, and maintainability
- Enables scalability - Consistent enforcement across all protected resources
- AuthZEN-compliant PEP — Delegates authorization decisions to any AuthZEN-compatible PDP for APIs and MCP requests
Features:
- JWT-Based Subject Identification — Extracts subject identity dynamically from JWT claims (e.g.,
sub,preferred_username) - Dynamic JWT Context Extraction — Maps JWT claims into AuthZEN
subject.propertiesfor attribute-based and relationship-based authorization - Dynamic Resource & Action Mapping — Builds AuthZEN resource identifiers from request URI, static values, or MCP context; maps HTTP methods or MCP tool arguments to actions
- Authenticated PDP Calls — Supports static API keys and OAuth2 credentials for gateway → PDP authentication, with automatic token caching and renewal
- MCP-Aware Policy Enforcement — Understands MCP JSON-RPC requests; enforces per-tool authorization with selective enforcement on specific methods
- OIDC/OAuth Integration — Works alongside the APISIX OIDC plugin; assumes tokens are validated upstream before authorization
- OpenFGA - Relationship-based access control (ReBAC)
- Cerbos - Policy-based access control (PBAC)
- Keycloak - Resource-based policies authorization
Should be compatible with Any AuthZEN-compliant PDP.
Access control failures are the #1 security risk in the OWASP Top 10 (2021). Traditional authorization approaches suffer from:
- Lack of Interoperability - Custom implementations lead to high maintenance costs
- Authorization Complexity - Traditional models are hard to scale
- Tight Coupling - Authorization logic embedded in application code reduces flexibility
The OpenID Foundation AuthZEN Working Group addresses these challenges by standardizing authorization interactions based on the P*P architecture principles, enabling interoperability between Policy Enforcement Points (PEP) and Policy Decision Points (PDP).
┌────────────────────────────────────────────────────────────┐
│ Authorization │
│ ┌─────────────────┐ │
│ │ AuthZEN PDP │ │
│ │ │ │
│ └─────┬───────────┘ │
│ │ │
evaluation() │ result { decision: true/false } │
│ │ │
│ │ │
│ ▼ │
┌──────────┐ │ ┌─────────────────┐ ┌───────────┐ │
│ │ Request │ │ │ Request │ │ │
│ Apps │ ──────────────► │ API/AI Gateway │ ─────────────────► │ API/MCP │ │
│ │ │ │ (AuthZEN PEP) │ │ │ │
└──────────┘ │ └─────────────────┘ └───────────┘ │
│ APISIX │
└────────────────────────────────────────────────────────────┘
AuthZEN Request (PEP → PDP):
{
"subject": { "type": "...", "id": "..." , "properties" : {} },
"resource": { "type": "...", "id": "...", "properties" : {} } ,
"action": { "name": "..." },
"context" : { }
}AuthZEN Response (PDP → PEP):
{
"decision": true
}sequenceDiagram
participant App
participant PEP as API / AI Gateway <br/> (AuthZEN PEP)
participant PDP as AuthZEN PDP
participant API as API / MCP
App->>PEP: Sends API request
PEP->>PEP: Authenticate Request
PEP->>PEP: Extract details (subject, method, resource)
PEP->>PDP: Sends AuthZEN request
PDP->>PDP: Evaluates policy
PDP-->>PEP: Returns allow/deny decision
alt Allow
PEP->>API: Forwards request
API-->>PEP: Returns response
PEP-->>App: Returns response
else Deny
PEP-->>App: Returns 403 Forbidden
end
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| pdp | object | Yes | PDP configuration | |
| pdp.host | string | Yes | PDP base URL (e.g., https://pdp.example.com) |
|
| pdp.platform | string | No | default |
Platform type: default, openfga, cerbos |
| pdp.model | string | No | discover |
For OpenFGA: discover to auto-discover store_id, or specify store_id directly |
| pdp.auth | object | No | PDP authentication configuration | |
| pdp.auth.api_key | string | No | Static API key sent as Authorization header (mutually exclusive with oauth2) |
|
| pdp.auth.oauth2 | object | No | OAuth2 client_credentials config for gateway→PDP auth (mutually exclusive with api_key) |
|
| pdp.auth.oauth2.token_url | string | Yes* | Token endpoint URL (e.g., Keycloak token endpoint) | |
| pdp.auth.oauth2.client_id | string | Yes* | OAuth2 client ID | |
| pdp.auth.oauth2.client_secret | string | Yes* | OAuth2 client secret (encrypted at rest) | |
| pdp.auth.oauth2.scopes | array | No | Requested token scopes (space-joined) | |
| http | object | No | HTTP client configuration | |
| http.timeout | integer | No | 3000 |
Request timeout in milliseconds (1-60000) |
| http.ssl_verify | boolean | No | false |
Enable SSL certificate verification |
| http.keepalive | boolean | No | true |
Enable HTTP keepalive connections |
| http.keepalive_timeout | integer | No | 60000 |
Keepalive timeout in milliseconds |
| http.keepalive_pool | integer | No | 5 |
Keepalive connection pool size |
| subject | object | No | Subject configuration for AuthZEN request | |
| subject.type | string | No | identity |
Subject type in AuthZEN request |
| subject.id | string | No | claim::sub |
Subject ID source (see Value Extraction Syntax) |
| subject.properties | array | No | Array of JWT claim mappings to subject properties | |
| resource | object | No | Resource configuration for AuthZEN request | |
| resource.type | string | No | route |
Resource type in AuthZEN request |
| resource.id | string | No | uri |
Resource ID source (see Value Extraction Syntax) |
| action | object | No | Action configuration for AuthZEN request | |
| action.name | string | No | method |
Action name source: method for HTTP method (see Value Extraction Syntax) |
| mcp | object | No | MCP (Model Context Protocol) configuration | |
| mcp.enforce_on | object | No | Selective enforcement configuration | |
| mcp.enforce_on.methods | array | No | [] |
MCP JSON-RPC methods to enforce authorization on (e.g., ["tools/call"]). If empty, all requests are enforced. |
Each item in subject.properties array:
| Name | Type | Required | Description |
|---|---|---|---|
| key | string | Yes | Property name in AuthZEN request |
| claim | string | Yes | JWT claim path (e.g., realm_access.roles, tenant) |
The subject.id, resource.id, and action.name fields support dynamic value extraction using the following syntax:
| Syntax | Source | Example |
|---|---|---|
claim::<name> |
JWT claim | claim::sub, claim::preferred_username |
mcp::tool::name |
MCP tool name from JSON-RPC body | Tool name from params.name |
mcp::tool::arguments::<arg> |
MCP tool argument | mcp::tool::arguments::tenant |
uri |
HTTP request URI | /api/v1/products |
method |
HTTP method | GET, POST |
| (static value) | Literal string | user, document, read |
IMPORTANT: This plugin assumes a valid JWT. You must configure the APISIX OIDC plugin with higher priority to validate the token before AuthZEN authorization is evaluated.
The plugin constructs AuthZEN-compliant requests following the specification:
{
"subject": {
"type": "<subject_type>",
"id": "<subject_identifier>",
"properties": {
"<key>": "<claim-value>"
}
},
"resource": {
"type": "<resource_type>",
"id": "<resource_identifier>"
},
"action": {
"name": "<action_name>"
}
}The plugin expects a standard AuthZEN response:
{
"decision": true
}| Response | HTTP Status | Description |
|---|---|---|
decision: true |
Request proceeds | Access granted |
decision: false |
403 Forbidden | Access denied |
| PDP unavailable | 503 Service Unavailable | Authorization service error |
| Missing JWT claim | 401 Unauthorized | Required claim not found |
Caution
Beta Software Notice: This software is currently in beta and is provided AS IS without any warranties.
- Not recommended for production use
- Issues and feedback should be reported via the GitHub issue tracker
- Maintenance and response times are best-effort
By using this beta software, you acknowledge and accept these conditions.
Example 1: Default Platform (Gateway Profile)
Minimal configuration using defaults - ideal for standard AuthZEN PDPs:
{
"pdp": {
"host": "https://pdp.example.com"
}
}This uses the default AuthZEN Gateway Profile:
- Subject:
type: "identity",id: <JWT sub claim> - Resource:
type: "route",id: <request URI> - Action:
name: <HTTP method>
AuthZEN request sent to PDP:
{
"subject": {
"type": "identity",
"id": "214cc559-1bd1-4436-ab82-621f3a414b34"
},
"resource": {
"type": "route",
"id": "/api/protected"
},
"action": {
"name": "GET"
}
}Example 2: OpenFGA with Auto-Discovery
For OpenFGA with automatic store_id discovery:
{
"pdp": {
"host": "http://localhost:8080",
"platform": "openfga",
"model": "discover"
}
}The plugin will:
- Call
GET {host}/storesto discover available stores - Use the first store's ID
- Cache the store_id for subsequent requests
- Send requests to:
POST {host}/stores/{store_id}/access/v1/evaluation
Example 3: OpenFGA with Explicit Store ID
Skip discovery by specifying the store_id directly:
{
"pdp": {
"host": "http://localhost:8080",
"platform": "openfga",
"model": "01JNW1803442023HVDKV03FB3A"
}
}Endpoint used: POST http://localhost:8080/stores/01JNW1803442023HVDKV03FB3A/access/v1/evaluation
Example 4: Cerbos with Subject Properties
Include JWT claims as subject properties for attribute-based decisions:
{
"pdp": {
"host": "http://localhost:3593",
"platform": "cerbos"
},
"subject": {
"type": "user",
"id": "claim::sub",
"properties": [
{ "key": "roles", "claim": "realm_access.roles" },
{ "key": "tenant", "claim": "tenant" },
{ "key": "email", "claim": "email" }
]
},
"resource": {
"type": "document",
"id": "uri"
},
"action": {
"name": "method"
}
}AuthZEN request sent to PDP:
{
"subject": {
"type": "user",
"id": "214cc559-1bd1-4436-ab82-621f3a414b34",
"properties": {
"roles": ["admin", "user"],
"tenant": "acme",
"email": "admin@example.com"
}
},
"resource": {
"type": "document",
"id": "/api/documents/123"
},
"action": {
"name": "GET"
}
}Example 5: With Static API Key and HTTP Settings
For PDPs requiring authentication with a static API key:
{
"pdp": {
"host": "https://pdp.example.com",
"platform": "default",
"auth": {
"api_key": "Bearer your-api-key-here"
}
},
"http": {
"timeout": 5000,
"ssl_verify": true,
"keepalive": true,
"keepalive_timeout": 60000,
"keepalive_pool": 10
}
}Example 6: MCP Tool Authorization
Authorize AI/MCP tool calls:
Plugin Configuration:
{
"pdp": {
"host": "http://localhost:8080",
},
"subject": {
"type": "user",
"id": "claim::sub"
},
"resource": {
"type": "tool",
"id": "mcp::tool::name"
},
"action": {
"name": "execute"
}
}MCP Request Body:
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "fintech_list_expenses",
"arguments": {
"tenant": "tenant1"
}
}
}AuthZEN request sent to PDP:
{
"subject": {
"type": "user",
"id": "214cc559-1bd1-4436-ab82-621f3a414b34"
},
"resource": {
"type": "tool",
"id": "fintech_list_expenses"
},
"action": {
"name": "execute"
}
}Example 7: MCP Selective Enforcement (Only tools/call)
Enforce authorization only for tools/call requests, allowing other MCP methods like initialize and tools/list to pass through without authorization:
Plugin Configuration:
{
"pdp": {
"host": "http://localhost:8080",
},
"mcp": {
"enforce_on": {
"methods": ["tools/call"]
}
},
"subject": {
"type": "user",
"id": "claim::sub"
},
"resource": {
"type": "mcp",
"id": "bank"
},
"action": {
"name": "mcp::tool::name"
}
}Behavior:
| MCP Method | Authorization |
|---|---|
tools/call |
Enforced |
initialize |
Skipped (allowed) |
tools/list |
Skipped (allowed) |
resources/list |
Skipped (allowed) |
MCP Request (tools/call - enforced):
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "fintech_list_expenses",
"arguments": { "tenant": "tenant1" }
}
}→ Authorization check performed, AuthZEN request sent to PDP
MCP Request (initialize - skipped):
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {}
}
}→ Request passes through without authorization
Note: If the mcp.enforce_on configuration is not provided or methods is empty, all requests are enforced (backward compatible behavior).
Example 8: Keycloak Authorization Services as PDP (OAuth2 PDP Auth)
Use Keycloak's built-in AuthZEN endpoint (KC_FEATURES=authzen) as the PDP. The gateway authenticates to Keycloak using OAuth2 client_credentials — the token is fetched once, cached, and renewed automatically before expiry.
Prerequisites:
- Keycloak started with
KC_FEATURES=authzen - A dedicated gateway client (
gateway-client) withauthorizationServicesEnabled=trueand the relevant resources/policies configured
{
"pdp": {
"host": "http://keycloak:8080/realms/demo/authzen",
"auth": {
"oauth2": {
"token_url": "http://keycloak:8080/realms/demo/protocol/openid-connect/token",
"client_id": "gateway-client",
"client_secret": "gateway-secret"
}
}
},
"subject": {
"type": "user",
"id": "claim::preferred_username"
},
"resource": {
"id": "todo-1",
"type": "todo"
},
"action": {
"name": "can_read_todos"
}
}How it works:
- On first request, the plugin calls Keycloak's token endpoint with
grant_type=client_credentials - The resulting access token is cached (TTL =
expires_in - 10s) in the shared APISIX memory store - Subsequent requests reuse the cached token; it is refreshed automatically when the cache expires
- If token renewal fails (e.g., client disabled), APISIX returns
503 Service Unavailable - Keycloak evaluates the AuthZEN request against the policies defined in
gateway-client's Authorization Services
Enterprise support, SLAs, and commercial features are available via TwoGenIdentity. Originally designed and implemented by Martin Besozzi. Maintained under the TwoGenIdentity organization.
Copyright 2026 TwoGenIdentity. All Rights Reserved.