Skip to main content
Version: 1.0

First Integration

Prerequisite: Complete the Quickstart first. This guide builds on the clients and policy data you created there.

This guide shows how to register a Resource Server, obtain a token, call /introspect, and handle obligations — the core integration loop for any RS.

Core concepts

You need five concepts to get through this guide:

  • Identity Provider (IdP) — the OIDC provider where users authenticate (e.g., Okta, Azure AD)
  • Client — an application registered with PBAC that requests or validates tokens
  • Token — an OAuth access token issued by PBAC; carries scopes and policy decisions
  • Introspect — the API call a resource server makes to validate a token and get the policy decision
  • Policy Data — JSON configuration that defines what clients can do, loaded into OPA

Everything else (obligations, consent, delegation, token exchange, AuthZEN, software statements) is useful but not needed on day one. You can explore those in the Concepts section when you're ready.


Overview

The Resource Server integration flow:

1. RS registers as an OAuth client via the Admin API
2. RS obtains a PAT (Protection API Token) with `uma_protection` scope
3. Client app obtains an access token
4. Client calls RS
5. RS calls /introspect with the access token + transaction context
6. RS reads the decision (active/inactive, scopes, obligations) and enforces

Step 1: Register the resource server

Register the RS using the Admin API:

curl -s -X POST $PBAC_URL/admin/api/clients \
-H "X-Admin-API-Key: $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{
"clientId": "my-rs-001",
"clientSecret": "rs-secret-abc",
"clientType": "confidential",
"clientName": "My Resource Server",
"softwareStatement": {
"sub": "my-rs-001",
"grant_types": ["client_credentials"],
"granted_resources": [
{"type": "urn:as:introspect", "scopes": ["uma_protection"]}
]
}
}' | jq .

The softwareStatement declares what this client is entitled to — in this case, the uma_protection scope on urn:as:introspect, which grants the ability to call /introspect. OPA reads this statement at every token request.

For self-service or federated client registration, see Registering Software (DCR). For this guide, static registration via the Admin API is simpler.


Step 2: Obtain a PAT (RS token)

The RS authenticates with client credentials to get a token with uma_protection scope:

PAT=$(curl -s -X POST $PBAC_URL/token \
-u "my-rs-001:rs-secret-abc" \
-d "grant_type=client_credentials" \
-d "scope=uma_protection" \
-d "resource_types=urn:as:introspect" \
| jq -r .access_token)

echo "PAT: $PAT"

Step 3: Client app obtains an access token

The quickstart-app client (registered in the Quickstart) gets a token to access a resource:

ACCESS_TOKEN=$(curl -s -X POST $PBAC_URL/token \
-u "quickstart-app:app-secret" \
-d "grant_type=client_credentials" \
-d "scope=read" \
-d "resource_types=urn:quickstart:data" \
| jq -r .access_token)
resource vs resource_types

The token endpoint accepts two resource parameters:

  • resource — a specific resource URI (RFC 8707), e.g., https://api.example.com/fhir. The AS resolves this to a type using registered Resource Servers.
  • resource_types — a type classification URN, e.g., urn:quickstart:data. Used when the type is known directly.

Both are valid. This guide uses resource_types because the examples work with type-level grants. In a real deployment with registered Resource Servers, you would typically use resource to target a specific API endpoint.


Step 4: RS calls /introspect

When the client presents its token, the RS introspects it — optionally providing transaction context (the action and resource being accessed):

curl -s -X POST $PBAC_URL/introspect \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "token=$ACCESS_TOKEN" \
-d "resource_types=urn:quickstart:data" \
-d "scopes=read" \
| jq .
ParameterRequired?Purpose
tokenYesThe access token to validate
resourceNoThe specific resource being accessed — triggers per-resource policy evaluation
resource_typesNoThe resource type — used for scope validation against the type registry
scopesNoThe actions being requested — OPA can narrow or deny based on these

Without the optional parameters, introspect returns the token's original grant. With them, OPA re-evaluates against the specific request context.

Active token response

{
"active": true,
"scope": "read",
"client_id": "quickstart-app",
"token_type": "Bearer",
"exp": 1700000300,
"iat": 1700000000,
"resource_types": ["urn:quickstart:data"],
"extensions": {
"granted_resource_types": ["urn:quickstart:data"],
"granted_resources": [
{
"type": "urn:quickstart:data",
"granted_actions": ["read"]
}
]
}
}

The extensions field contains PBAC-specific data: granted_resource_types (which types the token covers) and granted_resources (detailed type-level grants with actions). These are not standard OAuth fields — they help RSes verify the token was issued for their resource.

Inactive token response

{
"active": false
}

Step 5: Enforce the decision

The RS must:

  1. Check active: true — If false, reject the request with 401 Unauthorized.
  2. Verify scopes — Confirm the token's scope covers the requested action.
  3. Enforce obligations — If obligations is present, the RS must act on each one.

Obligations appear in the introspect response when your Rego policy extensions emit them via rs_obligations(). They are not built-in — you define them. For initial evaluation, you can skip obligations entirely. Here are common patterns you might implement later:

ObligationRS action
audit_level: "full"Log full request/response details
data_masking: ["ssn"]Redact SSN fields from the response
rate_limit: {"max": 100, "window": "1m"}Throttle requests to this client
notify: {"channel": "security"}Send alert to security channel

Here are complete examples showing the introspect-and-enforce pattern in Python, Node.js, and curl:

import requests

PBAC_URL = "https://pbac.example.com" # your PBAC instance
RS_CLIENT_ID = "my-rs-001"
RS_CLIENT_SECRET = "rs-secret-abc"

def introspect_token(access_token, resource_type, scope):
"""Call /introspect with transaction context."""
resp = requests.post(
f"{PBAC_URL}/introspect",
auth=(RS_CLIENT_ID, RS_CLIENT_SECRET),
data={
"token": access_token,
"resource_types": resource_type,
"scopes": scope,
},
)
resp.raise_for_status()
return resp.json()

def enforce_access(access_token, resource_type="urn:quickstart:data", scope="read"):
"""Validate token and enforce policy decision."""
result = introspect_token(access_token, resource_type, scope)

# 1. Check active
if not result.get("active"):
return 401, {"error": "invalid_token"}

# 2. Verify scope
granted_scopes = result.get("scope", "").split()
if scope not in granted_scopes:
return 403, {"error": "insufficient_scope"}

# 3. Handle obligations (if your policy emits them)
for resource in result.get("extensions", {}).get("granted_resources", []):
for obligation in resource.get("obligations", []):
if "audit" in obligation:
print(f"Audit: level={obligation['audit'].get('level')}")
if "rate_limit" in obligation:
print(f"Rate limit: {obligation['rate_limit']}")

return 200, {"message": "Access granted", "scope": result["scope"]}

JIT single-use tokens

If the OPA policy sets jit_single_use: true in the token response, the token is revoked after the first successful introspect. This is used for agent tokens where one-time access is required.

The RS sees a normal active token on first introspect. Subsequent introspects for the same token return active: false.


You should now understand:

  • Resource servers call /introspect to validate tokens
  • Introspect re-evaluates policy with RS-supplied transaction context
  • The RS reads active, scope, and obligations and enforces the decision
  • Obligations are custom and opt-in — defined in Rego extensions

Next steps