Policy Data Guide: Basic Policies Enabled by the Policy Data Structure
You are here: Full reference for every policy data key and what it controls. For a hands-on tutorial, start with Your first policy. For the conceptual model, see Policy engine.
This guide covers the policy data structure — the JSON configuration loaded into OPA as data — and how it drives registration, authorization, and token policies. You do not need to modify Rego rules; all examples are data-only changes.
1. Overview
Policy data lives in opa/data.json (or equivalent) and is loaded into OPA. The structure is concept-based:
data.oauth
├── denylist # Blocked entities (software, issuers, domains, clients, subjects)
├── trust # DCR: trusted issuers
├── resource # Resource types registry (type_uri → scopes)
├── client_id # Client entries keyed by client_id
├── software_id # Client entries keyed by software_statement.sub
├── user # IdP selection + subject restrictions
│ ├── idp_selection
│ └── subject_restrictions
└── flow # Endpoint-specific: register, authorize, token
The AS calls OPA with input; OPA evaluates policy rules against input + data and returns allow | deny | step_up plus obligations and context.
2. Trust issuers
What they are
Trust issuers are the OAuth/OIDC issuers you trust for federated operations. The trusted_issuers list is checked in two flows:
- Dynamic Client Registration (DCR): When a client registers with a software statement JWT, the
issclaim must match an entry intrusted_issuers. - id_token Token Exchange: When a client presents an id_token as a
subject_token(RFC 8693), the id_token'sissclaim must match a trusted issuer. This enables cross-domain identity flows where a user authenticated at one authorization server can obtain tokens at another.
How to add trust issuers
In data.oauth.trust.trusted_issuers, add the issuer URL(s):
{
"oauth": {
"trust": {
"trusted_issuers": [
"https://trust.example.com",
"https://federation.health.org",
"https://openbanking.org.uk"
]
},
"flow": {
"register": { "require_software_statement": true }
}
}
}
Policy behavior
- Register policy (
register.rego): Ifflow.register.require_software_statementistrueand a software statement is present, itsissmust be intrusted_issuers. Otherwise, registration is denied with reasonuntrusted_issuer. - If
flow.register.require_software_statementisfalseor unset, clients can register without a software statement (e.g. first-party admin-provisioned clients). - Token exchange policy (
token.rego): Whensubject_token_typeisid_tokenand the token'sissis not intrusted_issuers, the request is denied with reasonuntrusted_token_issuer.
3. Software statements: standard contract
What they are
A software statement is a JWT (or inline JSON) that describes the client's capabilities. It is the standard contract — functionality is directly derived from its claims, not from role lookup. The AS parses it and passes claims to the PDP as input.request.software_statement.
Direct derivation rules
| Software Statement Claim | Derives |
|---|---|
granted_resources includes urn:as:introspect with scope uma_protection | Access to UMA RS API |
grant_types includes refresh_token | Client may use/receive refresh tokens — must be confidential |
resources present (non-empty array) | Client is a Resource Server (hosts resources) |
granted_resources | What this client may request at token/authorize |
Consume vs Provide: granted_resources = what the client may request; resources = what the client hosts (RS only).
How to create a software statement (client app)
{
"iss": "https://federation.health.org",
"sub": "software-id-app-002",
"grant_types": ["authorization_code", "refresh_token"],
"granted_resources": [
{ "type": "urn:as:userinfo", "scopes": ["openid", "profile"] },
{ "type": "https://example.com/Patient", "scopes": ["read", "write"] },
{ "type": "https://example.com/Observation", "scopes": ["read"] },
{ "type": "https://example.com/Patient", "scopes": ["read"], "resource_indicator": "https://api.health.example.com/fhir" }
],
"resources": [],
"trust_level": "high"
}
iss— Must be intrust.trusted_issuerssub— Software ID; must not be indenylist.software_idsgrant_types— Grant types this client may use. If includesrefresh_token, client must be confidential.granted_resources— What this client may request:[{ type, scopes, resource_indicator? }]resources— (RS only) What this client hosts; empty for app clientstrust_level—high|medium|low;lowtriggers step-up
How to create a software statement (RS)
Access to UMA RS API is derived from granted_resources including urn:as:introspect with scope uma_protection:
{
"iss": "https://federation.health.org",
"sub": "software-id-rs-001",
"grant_types": ["client_credentials", "authorization_code"],
"granted_resources": [
{ "type": "urn:as:introspect", "scopes": ["uma_protection"] }
],
"resources": [
{ "type": "https://example.com/Patient", "scopes": ["read", "write"], "resource_indicator": "https://api.health.example.com/*" },
{ "type": "https://example.com/Observation", "scopes": ["read"], "resource_indicator": "https://api.health.example.com/*" }
],
"trust_level": "high"
}
How it's applied in register policy
- Denylist checks:
subnot indenylist.software_ids,issnot indenylist.issuers, redirect/client URIs not indenylist.domains - Trust:
issintrust.trusted_issuers - Explicit declarations:
grant_typesand at least one ofgranted_resourcesorresourcesmust be present - Step-up: If
trust_level == "low", policy returnsstep_upwith obligations - Registration context: On allow, policy returns
registrationwith entitlements from the software statement
4. Clients: entitlements and restrictions
Concept-based client model
Clients are defined in two keyed dictionaries: data.oauth.client_id (key = client_id) and data.oauth.software_id (key = software_statement.sub). Resolution order: client_id first, then software_id. First match wins.
Client entry shape
| Field | Purpose |
|---|---|
grant_types | Grant types this client may use |
granted_resources | [{ type, scopes, resource_indicator? }] — what this client may consume |
resources | (RS only) [{ type, scopes, resource_indicator }] — what this client hosts |
require_pkce | If true, this client must use PKCE (overrides flow-level) |
Per-software-ID override
"software_id": {
"demo-app-002": {
"granted_resources": [
{ "type": "https://example.com/Patient", "scopes": ["read"] },
{ "type": "https://example.com/Observation", "scopes": ["read"] }
]
}
}
Per-client-ID override
"client_id": {
"m2m-only-client": {
"grant_types": ["client_credentials"]
}
}
Resolution order
- Per client_id —
oauth.client_id[client_id](direct lookup) - Per software_id —
oauth.software_id[software_statement.sub] - Software statement — from registration (granted_resources, resources)
- Software statement — When no policy client matches, entitlements come solely from the software statement
5. Onboarding identity providers (IdPs)
IdP configuration
IdPs are stored in the identity_provider table. Each has a provider_key (e.g. mock-idp-default, health-idp) used in policy.
IdP selection via policy data
{
"oauth": {
"user": {
"idp_selection": {
"default_idp": "mock-idp-default",
"resource_to_idp": {
"https://api.health.example.com": "health-idp"
},
"resource_type_to_idp": {
"urn:example:health-record": "federated-idp"
},
"acr_to_idp": {
"mfa": "mfa-idp"
}
}
}
}
}
| Field | Purpose |
|---|---|
default_idp | Used when no resource/resource_type/acr match |
resource_to_idp | Map RFC 8707 resource URI → IdP provider_key |
resource_type_to_idp | Map resource type URI → IdP provider_key |
acr_to_idp | Map ACR value → IdP for step-up |
Selection priority: acr_to_idp → resource_to_idp → resource_type_to_idp → default_idp
Subject restrictions by IdP
{
"oauth": {
"user": {
"subject_restrictions": {
"idp:health-idp": {
"denied_scopes": ["admin"],
"denied_resource_types": []
}
}
}
}
}
Key format: idp:<provider_key>.
6. Managing the deny list
{
"oauth": {
"denylist": {
"software_ids": [],
"issuers": [],
"domains": [],
"client_ids": [],
"subjects": []
}
}
}
| List | Used By | Blocks |
|---|---|---|
software_ids | Register | Software statement sub |
issuers | Register | Software statement iss — entire federation |
domains | Register, Authorize | Redirect URI or client_uri host |
client_ids | Authorize, Token | OAuth client_id |
subjects | Token | User sub |
7. Flow-level settings
flow.register
{
"oauth": {
"flow": {
"register": {
"require_software_statement": true
}
}
}
}
flow.authorize
{
"oauth": {
"flow": {
"authorize": {
"require_pkce": false,
"require_pkce_for_public": true,
"require_pkce_for_clients": [],
"require_resource": false
}
}
}
}
| Field | Purpose |
|---|---|
require_pkce | Require PKCE globally |
require_pkce_for_public | Require PKCE for public clients |
require_pkce_for_clients | List of client_ids that must use PKCE |
require_resource | Deny when no resource or resource_types in authorize request |
8. Quick reference: policy data paths
| Path | Purpose |
|---|---|
oauth.denylist.software_ids | Block software statement sub |
oauth.denylist.issuers | Block software statement iss |
oauth.denylist.domains | Block redirect_uri / client_uri hosts |
oauth.denylist.client_ids | Block OAuth client_id |
oauth.denylist.subjects | Block user sub |
oauth.trust.trusted_issuers | Accept software statements from these issuers |
oauth.resource.types | Registry of resource type URIs → scopes |
oauth.client_id | Keyed dict: client_id → client entry |
oauth.software_id | Keyed dict: software_statement.sub → client entry |
oauth.user.idp_selection.default_idp | Default IdP when no resource match |
oauth.user.idp_selection.resource_to_idp | Resource URI → IdP |
oauth.user.idp_selection.resource_type_to_idp | Resource type URI → IdP |
oauth.user.idp_selection.acr_to_idp | ACR value → IdP (e.g. MFA) |
oauth.user.subject_restrictions | Per-subject or per-IdP scope/type restrictions |
oauth.flow.register.require_software_statement | Require software statement for registration |
oauth.flow.authorize.require_resource | Require resource in authorize |
oauth.flow.authorize.require_pkce | Require PKCE globally |
oauth.flow.authorize.require_pkce_for_public | Require PKCE for public clients |
Role library patterns
Use data.oauth.software_id entries to define reusable role archetypes:
Pattern: "Read-only API consumer"
{
"oauth": {
"software_id": {
"archetype:read-only-consumer": {
"grant_types": ["client_credentials"],
"granted_resources": [
{ "type": "urn:example:Patient", "scopes": ["read"] },
{ "type": "urn:example:Observation", "scopes": ["read"] }
]
}
}
}
}
Then in your software statement: "sub": "archetype:read-only-consumer".
Pattern: "Resource Server with introspection"
{
"oauth": {
"software_id": {
"archetype:fhir-rs": {
"grant_types": ["client_credentials"],
"granted_resources": [
{ "type": "urn:as:introspect", "scopes": ["uma_protection"] }
],
"resources": [
{ "type": "urn:example:Patient",
"scopes": ["read", "write"],
"resource_indicator": "https://api.example.com/fhir" }
]
}
}
}
}
Pattern: "Admin client with all access"
{
"oauth": {
"software_id": {
"archetype:admin-client": {
"grant_types": ["client_credentials", "authorization_code", "refresh_token"],
"granted_resources": [
{ "type": "urn:as:introspect", "scopes": ["introspection"] },
{ "type": "urn:as:authzen", "scopes": ["authzen"] },
{ "type": "urn:example:Patient", "scopes": ["read", "write", "delete"] }
]
}
}
}
}
Next steps
- Rego Primer — Write custom Rego extensions for logic that policy data alone cannot express
- Registering Software — How DCR clients use software statements evaluated against this data
- Concepts: Policy — The full policy model and extension hook architecture