I Built an OPA Plugin That Turns It Into an AuthZEN-Compatible PDP
<h1> Introduction </h1> <p>In my <a href="https://dev.to/kanywst/authzen-authorization-api-10-deep-dive-the-standard-api-that-separates-authorization-decisions-1m2a">previous article</a>, I did a deep dive into the AuthZEN Authorization API 1.0 spec. It standardizes communication between PEPs and PDPs. You send a JSON request asking "can this subject do this action on this resource?" and get back <code>{"decision": true/false}</code>.</p> <p>So the spec makes sense. But how do you actually use OPA as an AuthZEN-compatible PDP?</p> <p>OPA already has a REST API (<code>POST /v1/data/...</code>), but it doesn't match the AuthZEN API.</p> <ul> <li>Different path: AuthZEN uses <code>POST /access/v1/evaluation</code> </li> <li>Different request structure: OPA requires wrapping in <code>{"input":
Introduction
In my previous article, I did a deep dive into the AuthZEN Authorization API 1.0 spec. It standardizes communication between PEPs and PDPs. You send a JSON request asking "can this subject do this action on this resource?" and get back {"decision": true/false}.
So the spec makes sense. But how do you actually use OPA as an AuthZEN-compatible PDP?
OPA already has a REST API (POST /v1/data/...), but it doesn't match the AuthZEN API.
-
Different path: AuthZEN uses POST /access/v1/evaluation
-
Different request structure: OPA requires wrapping in {"input": {...}}
-
Different response structure: OPA returns {"result": ...}
There's an authzen-proxy in contrib, a Node.js proxy, but it requires a separate process.
So I built a plugin that runs the AuthZEN API directly inside the OPA process using OPA's plugin mechanism.
Repo: github.com/kanywst/opa-authzen-plugin
The OPA Community Discussion
Before getting into the code, some context on why this ended up as a plugin.
I opened an issue (#8449) on the OPA repository and brought it up in the #contributors Slack channel.
The Initial Proposal: Route Aliases
server: route_aliases: /access/v1/evaluation: /v1/data/authzen/allowserver: route_aliases: /access/v1/evaluation: /v1/data/authzen/allowEnter fullscreen mode
Exit fullscreen mode
The idea was to add configurable path mapping to the OPA server. I also put up a PR (#8451).
But path mapping alone can't handle request/response transformation, so it wouldn't fully satisfy the AuthZEN spec.
What the Community Said
The conclusion was:
-
Not in OPA core. OPA is a general-purpose policy engine used beyond just authorization. Adding use-case-specific features increases the surface area.
-
Plugin / distribution is the right approach. Same pattern as opa-envoy-plugin. Single binary, OPA + AuthZEN server.
-
Evaluation API is enough. The only REQUIRED endpoint in the well-known metadata is access_evaluation_endpoint. Evaluations (batch) and Search APIs are all OPTIONAL.
Nobody was against AuthZEN support itself. The stance was: start as a plugin, and if demand grows, it can move closer to core later.
There aren't many production AuthZEN users yet, so adding it to core didn't have strong justification at this point.
Architecture
Just like opa-envoy-plugin bundles OPA + Envoy External Authorization (gRPC) into a single binary, opa-authzen-plugin bundles OPA + an AuthZEN HTTP server into one.
Key points:
-
The AuthZEN request body (subject, resource, action, context) becomes OPA's input as-is. No wrapping needed.
-
The plugin evaluates data.. (default: data.authzen.allow) and returns the bool result as {"decision": ...}.
-
OPA's REST API (:8181) still works. Bundles, decision logs, and all other OPA features are available.
OPA's Plugin Mechanism
OPA lets you register plugins at runtime. Call runtime.RegisterPlugin with a Factory, and OPA will instantiate and start your plugin based on the config file.
The entrypoint is just this:
func main() { runtime.RegisterPlugin(plugin.PluginName, plugin.Factory{})func main() { runtime.RegisterPlugin(plugin.PluginName, plugin.Factory{})if err := cmd.RootCommand.Execute(); err != nil { os.Exit(1) } }`
Enter fullscreen mode
Exit fullscreen mode
opa-envoy-plugin uses the exact same pattern. runtime.RegisterPlugin to register a gRPC server plugin.
How AuthZEN Requests Reach OPA
An AuthZEN Access Evaluation API request looks like this:
{ "subject": {"type": "user", "id": "alice", "properties": {"role": "admin"}}, "resource": {"type": "document", "id": "doc-123"}, "action": {"name": "delete"}, "context": {"time": "2026-04-01T12:00:00Z"} }{ "subject": {"type": "user", "id": "alice", "properties": {"role": "admin"}}, "resource": {"type": "document", "id": "doc-123"}, "action": {"name": "delete"}, "context": {"time": "2026-04-01T12:00:00Z"} }Enter fullscreen mode
Exit fullscreen mode
The plugin passes this body directly as OPA's input. In Rego, you reference it like:
package authzen
default allow = false
allow if input.subject.properties.role == "admin"
allow if { input.action.name == "read" input.subject.id != "" }`
Enter fullscreen mode
Exit fullscreen mode
With OPA's Data API, you'd need to wrap it as {"input": {...}} and POST /v1/data/authzen/allow. The plugin handles that conversion internally.
Same for the response. OPA returns {"result": true}, and the plugin converts it to {"decision": true}.
Policy Dispatch
AuthZEN uses a single endpoint (/access/v1/evaluation) for all authorization decisions. If you want to route to different policies per resource type or action, you do that in Rego by checking input.resource.type or input.action.name.
package authzen
default allow = false
anyone can read todolists
allow if { input.resource.type == "todolist" input.action.name == "read" }
only editors can create todolists
allow if { input.resource.type == "todolist" input.action.name == "create" input.subject.properties.role == "editor" }
only admins can manage accounts
allow if { input.resource.type == "account" input.action.name == "manage" input.subject.properties.role == "admin" }`
Enter fullscreen mode
Exit fullscreen mode
As mentioned in the community discussion, PDP-specific hints (like which policy to evaluate) can also be passed via the context object:
{ "subject": {"type": "user", "id": "alice"}, "action": {"name": "read"}, "resource": {"type": "todolist", "id": "1"}, "context": {"policy_hint": "todolist"} }{ "subject": {"type": "user", "id": "alice"}, "action": {"name": "read"}, "resource": {"type": "todolist", "id": "1"}, "context": {"policy_hint": "todolist"} }Enter fullscreen mode
Exit fullscreen mode
Well-Known Metadata
The PDP metadata endpoint defined in Section 9 of the spec. PEPs can use it to dynamically discover which endpoints are available.
$ curl -s http://localhost:9292/.well-known/authzen-configuration | jq . { "policy_decision_point": "http://localhost:9292", "access_evaluation_endpoint": "http://localhost:9292/access/v1/evaluation" }$ curl -s http://localhost:9292/.well-known/authzen-configuration | jq . { "policy_decision_point": "http://localhost:9292", "access_evaluation_endpoint": "http://localhost:9292/access/v1/evaluation" }Enter fullscreen mode
Exit fullscreen mode
OPTIONAL endpoints (access_evaluations_endpoint, search_) aren't returned since they're not implemented. Per the spec, parameters with no value MUST be omitted._
Try It Out
Build and Run
git clone https://github.com/kanywst/opa-authzen-plugin.git cd opa-authzen-plugin make build ./opa-authzen-plugin run --server \ --config-file example/config.yaml \ example/policy.regogit clone https://github.com/kanywst/opa-authzen-plugin.git cd opa-authzen-plugin make build ./opa-authzen-plugin run --server \ --config-file example/config.yaml \ example/policy.regoEnter fullscreen mode
Exit fullscreen mode
Also works with Docker:
make docker-build make docker-runmake docker-build make docker-runEnter fullscreen mode
Exit fullscreen mode
Send Requests
Admin user deleting a document (allowed):
$ curl -s -X POST http://localhost:9292/access/v1/evaluation \ -H "Content-Type: application/json" \ -d '{ "subject": {"type": "user", "id": "alice", "properties": {"role": "admin"}}, "resource": {"type": "document", "id": "doc-123"}, "action": {"name": "delete"} }' {"decision":true}$ curl -s -X POST http://localhost:9292/access/v1/evaluation \ -H "Content-Type: application/json" \ -d '{ "subject": {"type": "user", "id": "alice", "properties": {"role": "admin"}}, "resource": {"type": "document", "id": "doc-123"}, "action": {"name": "delete"} }' {"decision":true}Enter fullscreen mode
Exit fullscreen mode
Regular user deleting a document (denied):
$ curl -s -X POST http://localhost:9292/access/v1/evaluation \ -H "Content-Type: application/json" \ -d '{ "subject": {"type": "user", "id": "bob", "properties": {"role": "viewer"}}, "resource": {"type": "document", "id": "doc-123"}, "action": {"name": "delete"} }' {"decision":false}$ curl -s -X POST http://localhost:9292/access/v1/evaluation \ -H "Content-Type: application/json" \ -d '{ "subject": {"type": "user", "id": "bob", "properties": {"role": "viewer"}}, "resource": {"type": "document", "id": "doc-123"}, "action": {"name": "delete"} }' {"decision":false}Enter fullscreen mode
Exit fullscreen mode
The X-Request-ID header is echoed back in the response (Section 10.1.3):
$ curl -si -X POST http://localhost:9292/access/v1/evaluation \ -H "Content-Type: application/json" \ -H "X-Request-ID: req-abc-123" \ -d '{ "subject": {"type": "user", "id": "alice", "properties": {"role": "admin"}}, "resource": {"type": "document", "id": "1"}, "action": {"name": "read"} }' HTTP/1.1 200 OK Content-Type: application/json X-Request-Id: req-abc-123$ curl -si -X POST http://localhost:9292/access/v1/evaluation \ -H "Content-Type: application/json" \ -H "X-Request-ID: req-abc-123" \ -d '{ "subject": {"type": "user", "id": "alice", "properties": {"role": "admin"}}, "resource": {"type": "document", "id": "1"}, "action": {"name": "read"} }' HTTP/1.1 200 OK Content-Type: application/json X-Request-Id: req-abc-123{"decision":true}`
Enter fullscreen mode
Exit fullscreen mode
What's Next
This is a PoC with just the Evaluation API.
-
Evaluations API (batch): OPTIONAL, but there's demand. Topaz has 1000+ edge users asking for it.
-
Search APIs: Hard to implement generically on top of OPA (ABAC). Partial evaluation might work, but nobody has explored that yet.
-
gRPC: There's interest in having gRPC alongside REST, but REST comes first.
Depending on how the OPA community responds, this could move to contrib or be integrated with opa-envoy-plugin.
Links
-
Repo: github.com/kanywst/opa-authzen-plugin
-
OPA Issue: open-policy-agent/opa#8449
-
AuthZEN Spec: openid.net/specs/authorization-api-1_0.html
-
Previous Article (AuthZEN Deep Dive): dev.to/kanywst/authzen-authorization-api-10-deep-dive
DEV Community
https://dev.to/kanywst/i-built-an-opa-plugin-that-turns-it-into-an-authzen-compatible-pdp-i81Sign in to highlight and annotate this article

Conversation starters
Daily AI Digest
Get the top 5 AI stories delivered to your inbox every morning.
More about
availableversionproductA Very Fine Untuning
How fine-tuning made my chatbot worse (and broke my RAG pipeline) I spent weeks trying to improve my personal chatbot, Virtual Alexandra , with fine-tuning. Instead I got increased hallucination rate and broken retrieval in my RAG system. Yes, this is a story about a failed attempt, not a successful one. My husband and I called fine tuning results “Drunk Alexandra” — incoherent answers that were initially funny, but quickly became annoying. After weeks of experiments, I reached a simple conclusion: for this particular project, a small chatbot that answers questions based on my writing and instructions, fine tuning was not a good option. It was not just unnecessary, it actively degraded the experience and didn’t justify the extra time, cost, or complexity compared to the prompt + RAG system

Writing Better RFCs and Design Docs
<p>RFCs (Request for Comments) and design docs are how engineering teams align on the “what” and “why” before writing code. Done well, they reduce rework and create a record of decisions. Done poorly, they sit unread or trigger endless debate. Here’s how to write <strong>better RFCs and design docs</strong> that get read, get feedback, and lead to decisions.</p> <h2> Why Write Them at All? </h2> <ul> <li> <strong>Alignment:</strong> Everyone works from the same understanding of the problem and the approach.</li> <li> <strong>Async review:</strong> People can respond in their own time, including across time zones.</li> <li> <strong>Memory:</strong> Later you have a record of why you chose X and what you rejected.</li> <li> <strong>Onboarding:</strong> New joiners (and future you) can unders

Building Global Crisis Monitor: A Real-Time Geopolitical Intelligence Dashboard
<p><a href="https://global-crisis-monitor.com" rel="noopener noreferrer"><strong>Global Crisis Monitor</strong></a> is a personal, artistic project. I built it in a period when wars that once felt distant became part of everyday conversation-appearing in feeds and notifications alongside everything else. There is something disorienting about that: a bombing in a city you can name, a ceasefire that collapsed overnight, a famine declared-and then, scrolling past it, an advertisement. The architecture of attention flattens everything into the same urgency and the same forgettability.</p> <p>I wanted to refuse that flattening. Not a feed aggregator; a single surface where the signals are collected, held together, and given weight. So I built an ingester that turns 80+ RSS feeds into structured
Knowledge Map
Connected Articles — Knowledge Graph
This article is connected to other articles through shared AI topics and tags.
More in Products

Writing Better RFCs and Design Docs
<p>RFCs (Request for Comments) and design docs are how engineering teams align on the “what” and “why” before writing code. Done well, they reduce rework and create a record of decisions. Done poorly, they sit unread or trigger endless debate. Here’s how to write <strong>better RFCs and design docs</strong> that get read, get feedback, and lead to decisions.</p> <h2> Why Write Them at All? </h2> <ul> <li> <strong>Alignment:</strong> Everyone works from the same understanding of the problem and the approach.</li> <li> <strong>Async review:</strong> People can respond in their own time, including across time zones.</li> <li> <strong>Memory:</strong> Later you have a record of why you chose X and what you rejected.</li> <li> <strong>Onboarding:</strong> New joiners (and future you) can unders

Building Global Crisis Monitor: A Real-Time Geopolitical Intelligence Dashboard
<p><a href="https://global-crisis-monitor.com" rel="noopener noreferrer"><strong>Global Crisis Monitor</strong></a> is a personal, artistic project. I built it in a period when wars that once felt distant became part of everyday conversation-appearing in feeds and notifications alongside everything else. There is something disorienting about that: a bombing in a city you can name, a ceasefire that collapsed overnight, a famine declared-and then, scrolling past it, an advertisement. The architecture of attention flattens everything into the same urgency and the same forgettability.</p> <p>I wanted to refuse that flattening. Not a feed aggregator; a single surface where the signals are collected, held together, and given weight. So I built an ingester that turns 80+ RSS feeds into structured

How We're Approaching a County-Level Education Data System Engagement
<p>When Los Angeles County needs to evaluate whether a multi-agency data system serving foster youth should be modernized or replaced, the work sits at the intersection of technology, policy, and people. That's exactly where we operate.</p> <h2> The Opportunity </h2> <p>The LA County Office of Child, Youth, and Family Well-Being is looking for a consulting team to analyze the Education Passport System (EPS), a shared data platform that connects 80+ school districts with the Department of Children and Family Services and the Probation Department. The system exists to ensure that when a foster youth moves between placements, their education records follow them.</p> <p>The question on the table: does the current system meet the needs of all stakeholders, or is it time to move to something new

I Built a Portable Text Editor for Windows — One .exe File, No Installation, Forever Free
<p>A solo developer's story of building the Notepad replacement that should have existed years ago.</p> <p>I've been using Windows my whole life. And my whole life, every time I needed to write something with a bit of formatting — a heading, some bold text, a colored note — I ended up either opening Word (too heavy), using Notepad (too limited), or pasting into a browser-based tool (too many accounts).</p> <p>WordPad was the middle ground. Then Microsoft removed it from Windows 11.<br> That was the moment I decided to build my own.</p> <h2> The Problem I Was Solving </h2> <p>Let me be specific about what I needed, because "text editor" covers everything from Vim to Google Docs.</p> <p>I wanted something that:</p> <ul> <li>Requires zero installation. I work on multiple machines — personal,

Discussion
Sign in to join the discussion
No comments yet — be the first to share your thoughts!