Skip to main content
Version: 1.0

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:

  1. It presents a Bearer token with uma_protection or introspection scope, or
  2. It authenticates with Basic auth and its software statement contains granted_resources with { "type": "urn:as:introspect", "scopes": ["uma_protection"] }, or
  3. OPA policy allows it a client_credentials token for urn: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"
}'
Extension architecture

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