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.
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_typesThe 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 .
| Parameter | Required? | Purpose |
|---|---|---|
token | Yes | The access token to validate |
resource | No | The specific resource being accessed — triggers per-resource policy evaluation |
resource_types | No | The resource type — used for scope validation against the type registry |
scopes | No | The 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:
- Check
active: true— If false, reject the request with401 Unauthorized. - Verify scopes — Confirm the token's
scopecovers the requested action. - Enforce obligations — If
obligationsis 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:
| Obligation | RS 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:
- Python
- Node.js
- 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"]}
const PBAC_URL = "https://pbac.example.com"; // your PBAC instance
const RS_CLIENT_ID = "my-rs-001";
const RS_CLIENT_SECRET = "rs-secret-abc";
async function introspectToken(accessToken, resourceType, scope) {
const resp = await fetch(`${PBAC_URL}/introspect`, {
method: "POST",
headers: {
Authorization:
"Basic " + Buffer.from(`${RS_CLIENT_ID}:${RS_CLIENT_SECRET}`).toString("base64"),
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
token: accessToken,
resource_types: resourceType,
scopes: scope,
}),
});
if (!resp.ok) throw new Error(`Introspect failed: ${resp.status}`);
return resp.json();
}
async function enforceAccess(accessToken, resourceType = "urn:quickstart:data", scope = "read") {
const result = await introspectToken(accessToken, resourceType, scope);
// 1. Check active
if (!result.active) return { status: 401, error: "invalid_token" };
// 2. Verify scope
const grantedScopes = (result.scope || "").split(" ");
if (!grantedScopes.includes(scope)) return { status: 403, error: "insufficient_scope" };
// 3. Handle obligations (if your policy emits them)
for (const resource of result.extensions?.granted_resources || []) {
for (const obligation of resource.obligations || []) {
if (obligation.audit) console.log("Audit:", obligation.audit);
if (obligation.rate_limit) console.log("Rate limit:", obligation.rate_limit);
}
}
return { status: 200, message: "Access granted", scope: result.scope };
}
# Introspect with Basic auth (alternative to Bearer PAT)
curl -s -X POST $PBAC_URL/introspect \
-u "my-rs-001:rs-secret-abc" \
-d "token=$ACCESS_TOKEN" \
-d "resource_types=urn:quickstart:data" \
-d "scopes=read" \
| jq .
# Check the response:
# - active: true → allow the request
# - active: false → return 401
# - scope field → verify it covers the requested action
# - extensions.granted_resources[].obligations → enforce each one
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
/introspectto validate tokens - Introspect re-evaluates policy with RS-supplied transaction context
- The RS reads
active,scope, andobligationsand enforces the decision - Obligations are custom and opt-in — defined in Rego extensions
Next steps
- How it works — Architecture and token lifecycle
- Enforcement — Obligations, JIT tokens, AuthZEN
- Policy — How OPA evaluates decisions