Your First Policy
You are here: Hands-on tutorial — write your first policy changes step by step. For the conceptual model, see Policy engine. For the full data key reference, see Policy data guide.
Prerequisite: Complete the Quickstart first. This guide builds on the clients and policy data you created there.
This guide walks through customizing policy — from simple data-driven changes to writing your first Rego deny rule.
How policy works
The AS evaluates policy at three key points:
/authorize— OPA decides whether to allow the authorization flow and which IdP to use./token— OPA decides whether to issue a token and what scopes to grant./introspect— OPA re-evaluates the token with RS-supplied action/resource context.
Policy has two parts: policy data (JSON you edit via the Admin API) and policy rules (Rego code that defines the evaluation logic). Most policy changes are data changes — update a JSON key and OPA picks it up within seconds. Rego rules are for custom logic that data can't express.
Data-driven policy changes
Most policy changes don't require writing Rego — you update a JSON data key via the Admin API.
Example: Deny a specific client
The simplest policy change is adding a client_id to the denylist. The PATCH API sets a single path within the oauth data key without overwriting sibling keys:
curl -s -X PATCH "$PBAC_URL/admin/api/policy-data/by-key?dataKey=oauth&path=denylist.client_ids" \
-H "X-Admin-API-Key: $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '["quickstart-app"]'
The built-in token policy checks data.oauth.denylist.client_ids automatically — you don't write Rego for this. Just update the data, and OPA denies the client on the next request. No code change, no restart.
Example: Restrict a user from a scope
Add a subject restriction to deny a specific user from requesting the admin scope:
curl -s -X PATCH "$PBAC_URL/admin/api/policy-data/by-key?dataKey=oauth&path=user.subject_restrictions.alice" \
-H "X-Admin-API-Key: $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"denied_scopes": ["admin"], "denied_resource_types": []}'
Example: Add a new resource type
Define a new resource type with scopes:
curl -s -X PATCH "$PBAC_URL/admin/api/policy-data/by-key?dataKey=oauth&path=resource.types.urn:example:patient" \
-H "X-Admin-API-Key: $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"scopes": ["read", "write", "delete"]}'
These examples use the PATCH API (?dataKey=oauth&path=...) which sets a value at a single dotted path without overwriting sibling keys. The quickstart used PUT for initial bulk seeding — but for incremental changes, always use PATCH to avoid accidentally wiping out existing data.
Verifying policy changes
After adding quickstart-app to the denylist above, wait 10-15 seconds for OPA to refresh, then verify the denial:
curl -s -X POST $PBAC_URL/token \
-u quickstart-app:app-secret \
-d 'grant_type=client_credentials&scope=read&resource_types=urn:quickstart:data' \
| jq .
You should see {"error":"access_denied",...}. OPA's denylist rule matched quickstart-app and denied the request. No code change, no restart — just a data update.
Clean up: Remove quickstart-app from the denylist so the next guide works:
curl -s -X PATCH "$PBAC_URL/admin/api/policy-data/by-key?dataKey=oauth&path=denylist.client_ids" \
-H "X-Admin-API-Key: $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '[]'
Wait 10-15 seconds, then confirm the client can get tokens again:
curl -s -X POST $PBAC_URL/token \
-u quickstart-app:app-secret \
-d 'grant_type=client_credentials&scope=read&resource_types=urn:quickstart:data' \
| jq .access_token
# Should return a token string (not an error)
Writing a custom Rego rule
For policy logic beyond data-driven changes — custom conditions, time-based access, context-aware decisions — you write Rego rules and load them via the Admin API.
Example: Deny resource evaluation during maintenance
The evaluations_ext extension is called during token issuance and introspection. Add a custom deny rule that blocks access based on a policy data flag:
The evaluations_ext package must define default for deny, rar_allow, rs_obligations, and subject_obligations. Omitting any of these causes OPA to reject the bundle, breaking all policy evaluation. Always include these four default lines.
curl -s -X PUT "$PBAC_URL/admin/api/policy-rules/by-path?path=oauth/evaluations_ext.rego" \
-H "X-Admin-API-Key: $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{
"path": "oauth/evaluations_ext.rego",
"version": "current",
"kind": "rego",
"enabled": true,
"content": "package oauth.evaluations_ext\n\nimport future.keywords.if\n\n# Required defaults — do not remove\ndefault deny(_, _, _, _) := false\ndefault rar_allow(_, _, _, _) := false\ndefault rs_obligations(_, _, _, _) := set()\ndefault subject_obligations(_, _, _, _) := set()\n\n# Custom rule: deny during maintenance\ndeny(resource, action, subject, context) if {\n data.oauth_config.custom.maintenance_mode == true\n}"
}' | jq .path
Here is the complete extension for readability:
package oauth.evaluations_ext
import future.keywords.if
# Required defaults — do not remove
default deny(_, _, _, _) := false
default rar_allow(_, _, _, _) := false
default rs_obligations(_, _, _, _) := set()
default subject_obligations(_, _, _, _) := set()
# Custom rule: deny during maintenance
deny(resource, action, subject, context) if {
data.oauth_config.custom.maintenance_mode == true
}
This rule denies all resource evaluations when maintenance_mode is true in your policy data. The AS merges extension rules with the base policy — no need to modify the built-in rules.
Why data.oauth_config? Extensions must use data.oauth_config instead of data.oauth to avoid an OPA recursion error. The AS serves a copy of your policy data at this path — same data, different key. See Policy: Extension hooks.
To test this rule: Add a custom key to your policy data with "maintenance_mode": true, then try requesting a token — it should be denied. Set it back to false and verify tokens work again.
See the Rego Primer for more examples and the full extension API.
You should now understand:
- Most policy changes are data changes (denylists, entitlements, restrictions)
- Rego extensions let you add custom deny rules, obligations, and TTL overrides
- Extensions use
data.oauth_config(notdata.oauth) to read policy data
For the full policy data structure and all configurable keys, see the Policy Data guide.