Policy engine: Rego and policy data
You are here: Conceptual overview — how OPA evaluates decisions and how policy data is structured. For a hands-on tutorial, start with Your first policy. For the full data key reference, see Policy data guide.
OPA evaluates every authorization decision in PBAC. The AS handles OAuth protocol mechanics — token issuance, introspection, client registration. OPA handles the authorization logic: should this be allowed, with what scopes, under what conditions.
Policy has two halves: Rego rules (the logic) and policy data (the configuration). Most day-to-day changes are data changes — updating a JSON key via the Admin API. Rego rules define the evaluation structure; policy data fills in the specifics.
Policy data
Policy data lives under data.oauth in OPA. It is stored as JSON in the database and served to OPA as a bundle. When you update policy data via the Admin API, OPA picks up the change on its next poll cycle (typically a few seconds).
data.oauth
├── denylist # Block specific clients, subjects, software IDs, issuers, domains
├── trust # Trusted software statement issuers for DCR
├── resource
│ └── types # Resource type registry (type URI → valid scopes)
├── client_id # Per-client entitlement overrides (keyed by issued client_id)
├── software_id # Per-software entitlement overrides (keyed by software statement sub)
├── user
│ ├── idp_selection # IdP routing rules (default, per-resource, per-ACR)
│ └── subject_restrictions # Per-user denied actions and types
├── flow
│ ├── register # Registration settings (require_software_statement, etc.)
│ ├── authorize # Authorize settings (PKCE requirements)
│ └── token # Token settings (extension config)
└── agents # Agent-specific settings (trusted/denied providers)
Update any key via the Admin API:
curl -X PUT "$PBAC_URL/admin/api/policy-data/by-key?dataKey=oauth" \
-H "X-Admin-API-Key: $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"dataKey": "oauth", "payload": "{...}"}'
How OPA resolves client entitlements
When a client requests a token, OPA determines what the client is entitled to using a three-step lookup:
data.oauth.client_id[<client_id>]— an explicit entry keyed by the issued client ID. Highest priority.data.oauth.software_id[<software_statement.sub>]— an entry keyed by the software identifier from the client's software statement.input.context.client.software_statement— the software statement claims stored on the client record. Lowest priority.
First match wins. The matched entry provides grant_types and granted_resources. If a field is absent, OPA falls back to the software statement claims.
In practice: most clients need no policy data entries at all. The software statement (step 3) carries the client's capabilities. You add client_id or software_id entries only when you need to override, restrict, or extend what the software statement says.
This means your initial setup can be very light: seed the resource types and trust issuers, and let software statements carry the rest. Policy data overrides are for when you need to tighten, loosen, or block specific clients after they're registered.
What OPA checks
At each endpoint, OPA evaluates a specific policy package:
| Endpoint | OPA path | Key checks |
|---|---|---|
/token | data.oauth.token | Client on denylist? Grant type allowed? Requested scopes covered by entitlements? Per-resource evaluation (subject restrictions, custom extensions). |
/authorize | data.oauth.authorize | Same as token, plus: redirect URI valid? PKCE required? Select IdP for redirect. |
/register | data.oauth.register | Software statement issuer trusted? Software ID on denylist? Required claims present? Redirect URI domains allowed? |
/introspect | data.oauth.token (re-evaluation) | Same token policy, but with RS-supplied context merged in. Can narrow scopes, add obligations, or deny based on the specific action. |
Denylists
Six denylist keys provide immediate block capabilities:
| Key | Blocks |
|---|---|
denylist.client_ids | Specific issued client IDs |
denylist.subjects | Specific user subject IDs |
denylist.software_ids | Software statement sub values |
denylist.issuers | Software statement issuer URLs |
denylist.domains | Redirect URI and client URI domains |
Denylist checks run first — before any entitlement evaluation. Adding a client ID to the denylist blocks it within seconds (next OPA bundle poll), no code deployment needed.
Denylists are your emergency brake. If a client or agent is compromised, one API call blocks it across every endpoint — no restart, no coordination with application teams.
Extension hooks
For custom logic beyond what policy data can express, PBAC provides two Rego extension points:
evaluations_ext — called during token issuance and introspection, per resource:
deny(resource, action, subject, context)— returntrueto denyrs_obligations(resource, actions, subject, context)— return obligations for the RSsubject_obligations(resource, actions, subject, context)— return obligations that pause the flow (e.g., consent redirect)expires_in_override/jit_single_use— modify token TTL or make it single-use
register_ext — called during DCR:
deny— returntrueto block registrationreason— deny reason string
Extensions are loaded as Rego policy rules via the Admin API, the same way as any policy rule. They use data.oauth_config instead of data.oauth to read policy data — this prevents OPA from detecting a recursive dependency. Same data, different path.
See the Rego Primer for syntax and practical examples.
Key principle
Most policy changes are data changes. Add a client to the denylist, update a software_id entitlement, change an IdP selection rule — all via the Admin API, all effective within seconds. Rego extensions are for custom logic that data-driven configuration can't express: time-based access, PIP calls, complex obligation conditions.
Next steps
- Enforcement — How Resource Servers validate tokens and enforce obligations
- Rego Primer — Writing custom policy extensions
- Policy Data guide — Full reference for all policy data keys