Token introspection
Token introspection (RFC 7662)
Resource servers call /introspect to validate tokens and receive policy-resolved claims.
Authentication: The caller must present a Bearer token with uma_protection or introspection scope, OR Basic auth with a client entitled to those scopes.
Basic introspection:
curl -X POST https://pbac.example.com/introspect \
-u "$RS_CLIENT_ID:$RS_CLIENT_SECRET" \
-d "token=$ACCESS_TOKEN"
Introspection with context (RS supplies the resource being accessed):
curl -X POST https://pbac.example.com/introspect \
-u "$RS_CLIENT_ID:$RS_CLIENT_SECRET" \
-d "token=$ACCESS_TOKEN" \
-d "resource=https://api.example.com/fhir/Patient/123" \
-d "resource_types=urn:example:Patient" \
-d "scopes=read"
Supplying context triggers a full re-evaluation of OPA token policy at introspect time, merging the RS-supplied context into the original PDP input. This enforces fine-grained access control at the point of enforcement.
Active token response:
{
"active": true,
"scope": "read",
"client_id": "550e8400-...",
"sub": "alice@example.com",
"token_type": "Bearer",
"exp": 1700003600,
"iat": 1700000000,
"extensions": {
"granted_resource": "https://api.example.com/fhir",
"granted_resource_types": ["urn:example:Patient"]
}
}
Inactive token response:
{ "active": false }
JIT single-use tokens: If OPA policy sets jit_single_use: true in the token response, the token is automatically revoked after the first successful introspection. Use this for one-time authorization flows.
Checking the caller's access to introspect:
A client can call /introspect if:
- It presents a Bearer token with
uma_protectionorintrospectionscope, or - It authenticates with Basic auth and its software statement contains
granted_resourceswith{ "type": "urn:as:introspect", "scopes": ["uma_protection"] }, or - OPA policy allows it a
client_credentialstoken forurn:as:introspect.
Custom enforcement policies in OPA
Policies live in OPA and are managed via the policy rule API.
Create a policy rule (stored in the DB and served in the OPA bundle):
curl -X POST https://pbac.example.com/admin/api/policy-rules \
-H "X-Admin-API-Key: $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Token extension: restrict by time",
"path": "policies/oauth/token_ext.rego",
"content": "package oauth.token.ext\n\nimport future.keywords.if\n\ndefault deny = false\n\ndeny if {\n now := time.now_ns() / 1000000000\n hour := (now % 86400) / 3600\n hour >= 22\n}\n\nreason := \"custom:outside_business_hours\" if deny\n"
}'
For the full extension hook contract, input schema, and architecture, see Concepts: Policy. For a Rego crash course with practical examples, see Rego for PBAC.
Subject restrictions via policy data (no Rego needed):
{
"oauth": {
"user": {
"subject_restrictions": {
"alice@example.com": {
"denied_scopes": ["admin"],
"denied_resource_types": []
},
"idp:external-idp": {
"denied_scopes": ["admin", "write"],
"denied_resource_types": ["urn:sensitive:type"]
}
}
}
}
}
Subject restrictions apply at both token issuance and introspection time.
Next steps
- Obligations — Subject obligations and RS obligations: structured format, decision types, and custom rules
- Rego Primer — Write custom deny rules and obligation extensions in Rego
- Concepts: Enforcement — DPoP validation, AuthZEN, and delegation patterns