Live
Black Hat USAAI BusinessBlack Hat AsiaAI BusinessNavigating the Challenges of Cross-functional Teams: the Role of Governance and Common GoalsDEV Community[Side B] Pursuing OSS Quality Assurance with AI: Achieving 369 Tests, 97% Coverage, and GIL-Free CompatibilityDEV Community[Side A] Completely Defending Python from OOM Kills: The BytesIO Trap and D-MemFS 'Hard Quota' Design PhilosophyDEV CommunityFrom Attention Economy to Thinking Economy: The AI ChallengeDEV CommunityHow We're Approaching a County-Level Education Data System EngagementDEV CommunityI Built a Portable Text Editor for Windows — One .exe File, No Installation, Forever FreeDEV CommunityBuilding Global Crisis Monitor: A Real-Time Geopolitical Intelligence DashboardDEV CommunityGoogle's TurboQuant saves memory, but won't save us from DRAM-pricing hellThe Register AI/MLWriting Better RFCs and Design DocsDEV CommunityAnthropic took down thousands of Github repos trying to yank its leaked source code — a move the company says was an accidentTechCrunchIntroducing The Screwtape LaddersLessWrong AIA Very Fine UntuningTowards AIBlack Hat USAAI BusinessBlack Hat AsiaAI BusinessNavigating the Challenges of Cross-functional Teams: the Role of Governance and Common GoalsDEV Community[Side B] Pursuing OSS Quality Assurance with AI: Achieving 369 Tests, 97% Coverage, and GIL-Free CompatibilityDEV Community[Side A] Completely Defending Python from OOM Kills: The BytesIO Trap and D-MemFS 'Hard Quota' Design PhilosophyDEV CommunityFrom Attention Economy to Thinking Economy: The AI ChallengeDEV CommunityHow We're Approaching a County-Level Education Data System EngagementDEV CommunityI Built a Portable Text Editor for Windows — One .exe File, No Installation, Forever FreeDEV CommunityBuilding Global Crisis Monitor: A Real-Time Geopolitical Intelligence DashboardDEV CommunityGoogle's TurboQuant saves memory, but won't save us from DRAM-pricing hellThe Register AI/MLWriting Better RFCs and Design DocsDEV CommunityAnthropic took down thousands of Github repos trying to yank its leaked source code — a move the company says was an accidentTechCrunchIntroducing The Screwtape LaddersLessWrong AIA Very Fine UntuningTowards AI

I Built an OPA Plugin That Turns It Into an AuthZEN-Compatible PDP

DEV Communityby ktApril 1, 20268 min read0 views
Source Quiz

<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/allow

Enter 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{})

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"} }

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"} }

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" }

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.rego

Enter fullscreen mode

Exit fullscreen mode

Also works with Docker:

make docker-build make docker-run

Enter 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}

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}

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

{"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

Was this article helpful?

Sign in to highlight and annotate this article

AI
Ask AI about this article
Powered by AI News Hub · full article context loaded
Ready

Conversation starters

Ask anything about this article…

Daily AI Digest

Get the top 5 AI stories delivered to your inbox every morning.

More about

availableversionproduct

Knowledge Map

Knowledge Map
TopicsEntitiesSource
I Built an …availableversionproductapplicationfeaturevaluationDEV Communi…

Connected Articles — Knowledge Graph

This article is connected to other articles through shared AI topics and tags.

Knowledge Graph100 articles · 188 connections
Scroll to zoom · drag to pan · click to open

Discussion

Sign in to join the discussion

No comments yet — be the first to share your thoughts!

More in Products