opa-authzen-plugin

module
v0.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 18, 2026 License: Apache-2.0

README

opa-authzen-plugin

CI Release Go Reference Go Report Card License

An extended version of OPA (OPA-AuthZEN) that implements the OpenID AuthZEN Authorization API 1.0 as a native OPA plugin.

Looking for opa-authzen? This is it. The repository and Go module are both named opa-authzen-plugin — import path github.com/kanywst/opa-authzen-plugin. The -plugin suffix is load-bearing: this is not a gateway or a sidecar in front of OPA, it is a plugin that makes OPA itself serve the AuthZEN endpoints.

Architecture

┌──────────────────────────────────────────────────┐
│               OPA Process (:8181)                │
│                                                  │
│  ┌────────────────────────────────────────────┐  │
│  │           OPA HTTP Server                  │  │
│  │                                            │  │
│  │  Built-in Routes       AuthZEN Routes      │  │
│  │  ┌──────────────┐  ┌───────────────────┐   │  │
│  │  │ POST /v1/    │  │ POST /access/v1/  │   │  │
│  │  │   data/...   │  │   evaluation      │   │  │
│  │  │ GET /health  │  │ POST /access/v1/  │   │  │
│  │  │ ...          │  │   evaluations     │   │  │
│  │  └──────────────┘  │ GET /.well-known/ │   │  │
│  │                    │   authzen-config. │   │  │
│  │                    └─────────┬─────────┘   │  │
│  └──────────────────────────────┼─────────────┘  │
│                                 │                │
│  ┌──────────────────────────────▼─────────────┐  │
│  │        AuthZEN Plugin (ExtraRoute)         │  │
│  │  ┌─────────┐  ┌──────────┐  ┌──────────┐   │  │
│  │  │ Validate│  │ Evaluate │  │ Metadata │   │  │
│  │  │ Request │─▶│  Policy  │  │ Endpoint │   │  │
│  │  └─────────┘  └─────┬────┘  └──────────┘   │  │
│  └─────────────────────┼──────────────────────┘  │
│                        │                         │
│  ┌─────────────────────▼──────────────────────┐  │
│  │          OPA Rego Engine + Store           │  │
│  │  ┌──────────┐  ┌───────────┐  ┌─────────┐  │  │
│  │  │ Compiler │  │ In-Memory │  │ Bundles │  │  │
│  │  │          │  │   Store   │  │         │  │  │
│  │  └──────────┘  └───────────┘  └─────────┘  │  │
│  └────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────┘

The plugin registers AuthZEN endpoints directly on OPA's own HTTP server (:8181) using OPA's ExtraRoute extension point. This means AuthZEN routes get OPA's built-in Prometheus metrics, OpenTelemetry tracing, and server authorization automatically — no separate port or listener required.

AuthZEN 1.0 Compliance

Spec Section Feature Status
Section 5 Information Model (Subject, Action, Resource, Context, Decision) ✅ Supported
Section 6 Access Evaluation API (POST /access/v1/evaluation) ✅ Supported
Section 7 Access Evaluations API (POST /access/v1/evaluations) ✅ Supported
Section 7.1.1 Default value merging for batch evaluations ✅ Supported
Section 7.1.2.1 Evaluation semantics (execute_all, deny_on_first_deny, permit_on_first_permit) ✅ Supported
Section 8 Search APIs (Subject, Resource, Action) ✅ Supported (opt-in via search config)
Section 8.2 Search pagination (stateless opaque token) ✅ Supported
Section 9 PDP Metadata (GET /.well-known/authzen-configuration) ✅ Supported
Section 10.1 HTTPS Transport Binding (JSON serialization, Content-Type validation) ✅ Supported
Section 10.1.3 X-Request-ID echo ✅ Supported
Section 11.7 Request payload protection (body size limit, batch size limit) ✅ Supported
Obligations Profile (Draft 1) Obligation discovery and PEP-capability negotiation ✅ Supported (opt-in via supported_obligations)
Access Request and Approval Profile (Draft 1) PDP discovery: access_request_endpoint, jwks_uri, capability URN ✅ Supported (opt-in) — see below
Access Request and Approval Profile (Draft 1) Access Request / Task Status endpoints, approval workflow, token signing ❌ Not implemented — the profile permits a separate service to host these

The Authorization API itself is final at 1.0. The two profiles are not: both are titled Draft 1 upstream, and the 1_0 in their filenames and URLs is the document name, not a released version. Support for them is opt-in and their normative text can still move — the denial-binding rules in the Access Request and Approval profile changed in September 2026, after this plugin shipped support for its discovery members.

Issue Management

Use GitHub Issues to request features or file bugs.

Quick Start

  1. Build the plugin.

    make build
    
  2. Create a policy file policy.rego:

    package authzen
    
    default allow = false
    
    allow if input.subject.properties.role == "admin"
    
    allow if {
        input.action.name == "read"
        input.subject.id != ""
    }
    
  3. Create a config file config.yaml:

    plugins:
      authzen:
        path: "authzen"
        decision: "allow"
    
  4. Run the plugin.

    ./opa-authzen-plugin run --server --config-file config.yaml policy.rego
    

    This starts OPA on :8181 with the AuthZEN endpoints registered on the same server.

  5. Send an AuthZEN evaluation request.

    curl -s -X POST http://localhost:8181/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"}
      }'
    

    The response should be:

    {"decision":true}
    
  6. Send a batch evaluation request.

    curl -s -X POST http://localhost:8181/access/v1/evaluations \
      -H "Content-Type: application/json" \
      -d '{
        "subject": {"type": "user", "id": "alice", "properties": {"role": "admin"}},
        "action": {"name": "read"},
        "evaluations": [
          {"resource": {"type": "document", "id": "doc-1"}},
          {"resource": {"type": "document", "id": "doc-2"}},
          {"action": {"name": "delete"}, "resource": {"type": "document", "id": "doc-3"}}
        ]
      }'
    

    The response should be:

    {"evaluations":[{"decision":true},{"decision":true},{"decision":true}]}
    

    Top-level subject, action, resource, and context serve as defaults for each item in the evaluations array. Individual items can override any of these fields. See Section 7 of the AuthZEN spec for details on evaluation semantics (execute_all, deny_on_first_deny, permit_on_first_permit).

  7. Use evaluation semantics to short-circuit batch processing.

    curl -s -X POST http://localhost:8181/access/v1/evaluations \
      -H "Content-Type: application/json" \
      -d '{
        "subject": {"type": "user", "id": "bob"},
        "action": {"name": "read"},
        "options": {"evaluations_semantic": "permit_on_first_permit"},
        "evaluations": [
          {"resource": {"type": "document", "id": "doc-1"}},
          {"resource": {"type": "document", "id": "doc-2"}}
        ]
      }'
    
  8. Check the well-known metadata endpoint.

    curl -s http://localhost:8181/.well-known/authzen-configuration | jq .
    

    Response:

    {
      "policy_decision_point": "http://localhost:8181",
      "access_evaluation_endpoint": "http://localhost:8181/access/v1/evaluation",
      "access_evaluations_endpoint": "http://localhost:8181/access/v1/evaluations"
    }
    

API Gateway Integration

opa-authzen-plugin can serve as a standards-based PDP behind any API gateway that supports external authorization. The example/envoy-gateway/ directory demonstrates this pattern with Envoy proxy:

Client → Envoy → ext-authz-bridge → opa-authzen-plugin (AuthZEN PDP)
                                          ↓
                                     OPA Rego policy

A thin translation layer (ext-authz-bridge) converts gateway-specific protocols (e.g., Envoy ext_authz gRPC) into AuthZEN evaluation requests. The PDP itself is gateway-agnostic — it can serve any AuthZEN-compatible PEP (Kong, AWS API Gateway, Tyk, etc.) without modification.

This differs from opa-envoy-plugin, which embeds Envoy's gRPC ext_authz protocol directly into OPA. opa-authzen-plugin uses the OpenID AuthZEN standard as the protocol between the gateway and PDP.

cd example/envoy-gateway
docker compose up --build
# Test
curl -i -H "X-User: rick" http://localhost:9000/todos    # → 200
curl -i -X POST -H "X-User: jerry" http://localhost:9000/todos  # → 403

Docker

make docker-build
make docker-run

The published image is ghcr.io/kanywst/opa-authzen-plugin. Pass --addr 0.0.0.0:8181 when you publish the port: OPA binds localhost by default, so a container started without it accepts no connections from outside itself.

docker run --rm -p 8181:8181 -v "$PWD/example:/example:ro" \
  ghcr.io/kanywst/opa-authzen-plugin:latest \
  run --server --addr 0.0.0.0:8181 --config-file /example/config.yaml /example/policy.rego

Images and release binaries are signed with Sigstore keyless signing and ship with an SBOM. See RELEASE.md for the verification commands.

Configuration

The plugin is configured under the plugins.authzen key in the OPA config file:

Key Type Default Description
path string authzen OPA package path to query
decision string allow Rule name within the package that produces the boolean decision
decision_context string (unset) Rule name whose object value is returned as the Decision's optional context
search.subject string (unset) Rule name returning the set of permitted subjects (enables Subject Search)
search.resource string (unset) Rule name returning the set of permitted resources (enables Resource Search)
search.action string (unset) Rule name returning the set of permitted actions (enables Action Search)
search.max_limit int 1000 Per-page cap; client page.limit is clamped to this value
capabilities array (unset) PDP capability URNs advertised in the capabilities field of the PDP metadata
supported_obligations array (unset) Obligation Types advertised in the PDP metadata; also bounds the PEP-declared set on each request
access_request_endpoint string (unset) HTTPS URI advertised as access_request_endpoint in the PDP metadata (Access Request and Approval Profile)
jwks_uri string (unset) HTTPS URI advertised as jwks_uri in the PDP metadata, for verifying signed values issued under that profile

If a search.* rule is unset, the corresponding endpoint responds with 501 and is omitted from the PDP metadata (spec Section 9). Each configured rule must be defined in the package given by path and return a set/array of entity objects.

When decision_context is set, the named rule is evaluated alongside the decision (under the same policy/data snapshot) and its result is returned as the Decision's optional context member (spec Section 5.5.1), letting a policy convey reasons, obligations, or other metadata. The rule must evaluate to a JSON object; an undefined result or an empty object omits context, and a non-object result fails the request. decision_context is unset by default, so responses carry only decision unless you opt in.

The AuthZEN core specification registers no capability URNs of its own (the IANA "AuthZEN Policy Decision Point Capabilities" registry is populated by profiles and vendors), so capabilities is operator-supplied. Each entry must be a URN (start with urn:); the list is omitted from the metadata when empty. A deployment running the Access Request and Approval profile advertises urn:openid:authzen:capability:access-request here — see below.

Obligations Profile

supported_obligations opts the PDP into the AuthZEN Profile for Obligations (Draft 1). Set it to the Obligation Types your policies may issue — a type registered in the "AuthZEN Obligation Types" registry (step-up, notification, session_termination) or the literal custom. The registry is IANA "Specification Required" and therefore extensible, so unregistered values are accepted; only empty entries are rejected at startup.

The list does two things. It is advertised as the supported_obligations member of the PDP metadata, so a PEP can discover which types it must be prepared to execute. And it bounds the PEP's own context.supported_obligations array on each request: the profile requires a PDP to ignore any declared value it did not itself advertise, so the plugin filters the member before the input reaches Rego. Your policy can therefore read input.context.supported_obligations and trust every entry is a type this PDP is configured to issue. A member that survives filtering as an empty array is kept — a PEP declaring only unsupported types is telling you something, which the profile distinguishes from an absent member — while a member that is not an array at all is removed.

The obligations themselves ride in the Decision context, which the decision_context rule supplies, so a policy issuing obligations sets both keys:

plugins:
  authzen:
    decision_context: obligations_ctx
    supported_obligations:
      - step-up
      - notification
package authzen

obligations_ctx := {"obligations": [{
    "id": "step-up-1",
    "type": "step-up",
    "properties": {"acr_value": "urn:com:example:loa:3"}
}]} if {
    not allow
    "step-up" in input.context.supported_obligations
}

Leaving supported_obligations unset means the PDP does not implement the profile: the metadata member is omitted and request context reaches the policy untouched. If your policy already reads input.context.supported_obligations for its own purposes, note that opting in changes what it sees.

The plugin paginates over the entire result set returned by the Rego rule, sorting by the entity's type+id (or name for actions) so page boundaries are stable across requests. If your search rules can return very large candidate sets (tens of thousands), prefer filtering inside Rego rather than relying on the plugin's per-page slicing — the rule still runs once per page request, so heavy candidate sets are paid for every call.

Access Request and Approval Profile

The Access Request and Approval Profile (Draft 1) turns a denial into something a PEP can act on: the PDP marks a denial as requestable, and the PEP submits an access request that a human or workflow can approve.

This plugin implements the PDP discovery half of the profile, not the Access Request Service. That split is one the profile itself allows — the Access Request Endpoint "MAY be hosted by the PDP itself, by a service trusted by the PDP, or by an independent service operating with delegated authority from the PDP". The endpoint, the Task Status Endpoint, and the approval workflow behind them are stateful, OAuth-protected services; this plugin advertises where they live and lets your policy emit the requestable-denial hint.

Two config keys feed the PDP metadata document:

plugins:
  authzen:
    decision_context: denial_ctx
    access_request_endpoint: "https://requests.example.com/access/v1/requests"
    jwks_uri: "https://requests.example.com/access/v1/jwks"
    capabilities:
      - "urn:openid:authzen:capability:access-request"

Both must be https:// URIs with a host; a bad value fails at startup rather than being published to every PEP that reads the metadata. Both are unset by default and omitted from the metadata when unset — and the absence of access_request_endpoint is exactly how a PEP learns this PDP advertises no Access Request Endpoint. jwks_uri is only needed when the deployment issues signed values under the profile, such as a JWS binding_token, so it can be left unset while the endpoint is advertised — but see the binding forms below: a deployment whose Access Request Service is independent of the PDP has to issue that token, and therefore has to publish the key.

The requestable-denial hint itself is policy output, carried by the existing decision_context rule. The example below binds the denial by reference, so it assumes the shared-state topology described under the binding forms: something outside the plugin must record what the Access Request Service will later resolve from evaluation_id, since a stateless OPA records nothing.

package authzen

denial_ctx := {
    "reason": "approval_required",
    "evaluation_id": input.context.request_id,
    "access_request": {
        "template": "manager_approval",
        "expires_at": "2026-09-02T20:25:00Z",
    },
} if {
    not allow
    input.resource.properties.sensitivity == "high"
}

The profile requires expires_at on every requestable denial, and requires denial-binding material in at least one of two forms, and which form is available to you depends on your topology. A stable evaluation_id binds by reference and applies only where the Access Request Service resolves it against state shared with, or delegated by, the PDP; a signed binding_token binds by value and works in any topology. Where the Access Request Service is independent of the PDP, the profile requires the binding_token form and requires that token to be self-contained — carrying the denied Subject, Resource, Action, and authorization-relevant Context inline or as a binding_hash — because a stateless PDP keeps no decision state for an independent service to fetch. A binding_token carrying only an evaluation_id does not satisfy that requirement. Emitting both forms is allowed and is worth doing: the profile still recommends a stable context.evaluation_id, and when a binding_token is present that identifier stops being a binding form and serves as a correlation and audit identifier instead. Both values come from your policy or a PDP-side signer; the plugin passes the decision_context object through unchanged and does not synthesize, sign, or validate them, so a deployment that needs the binding_token form must sign it outside the plugin and publish the verification key at jwks_uri. Emit context.access_request only when an Access Request Endpoint is actually able to process the request — the profile makes the presence of that object the sole signal that a denial is requestable.

API Reference

POST /access/v1/evaluation

Single access evaluation. Request body:

{
  "subject":  {"type": "user", "id": "alice@example.com"},
  "action":   {"name": "can_read"},
  "resource": {"type": "account", "id": "123"},
  "context":  {"time": "2024-10-26T01:22-07:00"}
}

Response: {"decision": true}

POST /access/v1/evaluations

Batch access evaluations with default value merging and evaluation semantics. See example/ for detailed usage.

A request that carries one or more evaluations is answered with {"evaluations": [...]}, one Decision per request item in the same order. A request whose evaluations array is absent or empty is a single Access Evaluation (spec Section 7.1), and is answered with the singular {"decision": ...} — not a one-element batch envelope.

POST /access/v1/search/subject

Subject Search (spec Section 8.4). Requires search.subject to be configured. Request body:

{
  "subject":  {"type": "user"},
  "action":   {"name": "can_read"},
  "resource": {"type": "account", "id": "123"},
  "page":     {"limit": 100}
}

Response:

{
  "page": {"next_token": "", "count": 2, "total": 2},
  "results": [
    {"type": "user", "id": "alice@example.com"},
    {"type": "user", "id": "bob@example.com"}
  ]
}
POST /access/v1/search/resource

Resource Search (spec Section 8.5). Requires search.resource. Same response shape as Subject Search; results are resource entities.

POST /access/v1/search/action

Action Search (spec Section 8.6). Requires search.action. Request omits the action key. results are action entities of shape {"name": "..."}.

GET /.well-known/authzen-configuration

PDP metadata discovery endpoint (Section 9).

License

Apache License 2.0. See LICENSE.

Directories

Path Synopsis
cmd

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL