Skip to main content
Version: 1.0

Policy Data Guide: Basic Policies Enabled by the Policy Data Structure

Three ways to learn about policy

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:

  1. Dynamic Client Registration (DCR): When a client registers with a software statement JWT, the iss claim must match an entry in trusted_issuers.
  2. id_token Token Exchange: When a client presents an id_token as a subject_token (RFC 8693), the id_token's iss claim 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): If flow.register.require_software_statement is true and a software statement is present, its iss must be in trusted_issuers. Otherwise, registration is denied with reason untrusted_issuer.
  • If flow.register.require_software_statement is false or unset, clients can register without a software statement (e.g. first-party admin-provisioned clients).
  • Token exchange policy (token.rego): When subject_token_type is id_token and the token's iss is not in trusted_issuers, the request is denied with reason untrusted_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 ClaimDerives
granted_resources includes urn:as:introspect with scope uma_protectionAccess to UMA RS API
grant_types includes refresh_tokenClient may use/receive refresh tokens — must be confidential
resources present (non-empty array)Client is a Resource Server (hosts resources)
granted_resourcesWhat 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 in trust.trusted_issuers
  • sub — Software ID; must not be in denylist.software_ids
  • grant_types — Grant types this client may use. If includes refresh_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 clients
  • trust_levelhigh | medium | low; low triggers 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

  1. Denylist checks: sub not in denylist.software_ids, iss not in denylist.issuers, redirect/client URIs not in denylist.domains
  2. Trust: iss in trust.trusted_issuers
  3. Explicit declarations: grant_types and at least one of granted_resources or resources must be present
  4. Step-up: If trust_level == "low", policy returns step_up with obligations
  5. Registration context: On allow, policy returns registration with 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

FieldPurpose
grant_typesGrant 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_pkceIf 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

  1. Per client_idoauth.client_id[client_id] (direct lookup)
  2. Per software_idoauth.software_id[software_statement.sub]
  3. Software statement — from registration (granted_resources, resources)
  4. 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"
}
}
}
}
}
FieldPurpose
default_idpUsed when no resource/resource_type/acr match
resource_to_idpMap RFC 8707 resource URI → IdP provider_key
resource_type_to_idpMap resource type URI → IdP provider_key
acr_to_idpMap ACR value → IdP for step-up

Selection priority: acr_to_idpresource_to_idpresource_type_to_idpdefault_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": []
}
}
}
ListUsed ByBlocks
software_idsRegisterSoftware statement sub
issuersRegisterSoftware statement iss — entire federation
domainsRegister, AuthorizeRedirect URI or client_uri host
client_idsAuthorize, TokenOAuth client_id
subjectsTokenUser 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
}
}
}
}
FieldPurpose
require_pkceRequire PKCE globally
require_pkce_for_publicRequire PKCE for public clients
require_pkce_for_clientsList of client_ids that must use PKCE
require_resourceDeny when no resource or resource_types in authorize request

8. Quick reference: policy data paths

PathPurpose
oauth.denylist.software_idsBlock software statement sub
oauth.denylist.issuersBlock software statement iss
oauth.denylist.domainsBlock redirect_uri / client_uri hosts
oauth.denylist.client_idsBlock OAuth client_id
oauth.denylist.subjectsBlock user sub
oauth.trust.trusted_issuersAccept software statements from these issuers
oauth.resource.typesRegistry of resource type URIs → scopes
oauth.client_idKeyed dict: client_id → client entry
oauth.software_idKeyed dict: software_statement.sub → client entry
oauth.user.idp_selection.default_idpDefault IdP when no resource match
oauth.user.idp_selection.resource_to_idpResource URI → IdP
oauth.user.idp_selection.resource_type_to_idpResource type URI → IdP
oauth.user.idp_selection.acr_to_idpACR value → IdP (e.g. MFA)
oauth.user.subject_restrictionsPer-subject or per-IdP scope/type restrictions
oauth.flow.register.require_software_statementRequire software statement for registration
oauth.flow.authorize.require_resourceRequire resource in authorize
oauth.flow.authorize.require_pkceRequire PKCE globally
oauth.flow.authorize.require_pkce_for_publicRequire 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