Rego input and output
This page lists what a custom control under pbac.controls can read, and what the token policy returns. The JSON samples are copied from audit rows of a test instance. The audit row keeps both, as policyInput and policyDecision.
Entry points
The authorization server asks OPA for a decision at these paths.
| Endpoint | OPA path | Runs custom controls |
|---|---|---|
/token | /v1/data/oauth/token | Yes |
/introspect | /v1/data/oauth/token, with the token's grant and the resource server's context added | Yes |
/authorize | /v1/data/oauth/authorize | Yes |
/register | /v1/data/oauth/register | No. Its hook is the shipped package oauth.register.ext. |
/access/v1/evaluation (AuthZEN) | /v1/data/direct/test | No. That package reads only the denylist control and the older oauth_config.token_policy.subject_restrictions key. |
What a control sees
The token and authorize policies call each control once for each resource and each requested action. Before each call they set these fields of input.
| Field | Contents |
|---|---|
input.resource | {type, id, properties} of one requested resource |
input.action | One requested scope, as a string. Read by deny and rar_allow. |
input.actions | Every requested scope for the resource. Read by rs_obligations and subject_obligations. |
input.subject | The user, or null for a client credentials token |
input.context | The request context, below |
expires_in_override and jit_single_use are read once for the whole request. Those rules see the request as sent, with input.resources and no input.resource.
Token request input
The input for a client credentials request, as the audit row stored it:
{
"resources": [
{"id": null, "type": "urn:example:reports", "actions": ["read", "write"], "properties": {}}
],
"subject": null,
"context": {
"client": {
"client_id": "c2dc0c12-90ac-460d-8a0c-b9b4b9ac706b",
"client_type": "confidential",
"redirect_uri": null,
"registration_type": "DCR_PLAIN",
"is_resource_server": false,
"software_statement": {},
"dpop_bound_access_tokens": false
},
"entrypoint": "token",
"grant_type": "client_credentials",
"environment": {"ip": "203.0.113.10", "user_agent": "curl/8.7.1"}
},
"dpop": {"jkt": null, "error": null, "valid": false, "present": false}
}
| Field | Contents |
|---|---|
resources[] | Each requested resource with its type, id (the resource indicator, or null), requested actions and properties. A resource the server inferred carries derived: true. |
subject | null for client credentials. For a user, sub, idp, acr and session_id, plus claims the identity provider passed. |
context.client.client_id | The client making the request |
context.client.client_type | confidential or public |
context.client.registration_type | How the client registered. We saw DCR_PLAIN for a client registered without a statement. |
context.client.software_statement | Every claim of the client's software statement, or {} |
context.entrypoint | token |
context.grant_type | The grant, such as client_credentials or urn:ietf:params:oauth:grant-type:token-exchange |
context.environment | The caller's ip and user_agent as the server saw them |
context.rs_context | Only at introspection. The context the resource server sent with the call. |
context.token_granted_resources | Only at introspection. What the token was granted at issuance. |
context.subject_token_granted_resources | Only in token exchange. What the subject token was granted. |
context.actor | Only in token exchange with an actor_token. The actor's client_id, and its sub when it has one. |
context.delegations | Delegation grants for the subject and resource, when there are any |
dpop | Whether the request carried a DPoP proof, and its key thumbprint |
Token decision output
The decision for a request that asked for read write while a control refused write:
{
"allow": true,
"reason": null,
"decision": "allow",
"deny_reasons": [
{"area": "pbac.controls.reports_read_only", "code": "reports_write_blocked", "action": "write", "resource": "urn:example:reports"}
],
"granted_resources": [
{"id": null, "type": "urn:example:reports", "scopes": null, "obligations": null, "granted_actions": ["read"]}
],
"rs_obligations": null,
"subject_obligations": [],
"expires_in_override": null,
"jit_single_use": null,
"matched_segments": ["dcr_clients"]
}
| Field | Contents |
|---|---|
allow | true when a token is issued |
reason | One reason for a refused request, such as all_resources_denied, client_denied or grant_type_not_allowed. null when allowed. |
deny_reasons | Every reason a control added, for allowed and refused requests alike |
granted_resources | Each granted resource with its granted_actions |
subject_obligations | Steps the user must complete, such as approval, before the token is issued |
rs_obligations | Obligations for the resource server. Only filled at introspection. |
expires_in_override | The token lifetime in seconds when a control set one. The shortest wins. |
jit_single_use | true when a control made the token single use |
matched_segments | The segments that held for this request |
Gateway calls and connector policy
When the managed gateway checks a call, it introspects the caller's token with the call's context. A rule that guards a connector must read that context, not input.resource. An integration test probes these fields on a running gateway.
| Field | On a gateway call |
|---|---|
input.context.rs_context.resource_type | The connector's resource type. Use it to scope a rule to one connector. |
input.context.rs_context.resource_name | The managed resource's name |
input.context.rs_context.resource_id | The raw value of the route's resource-id path parameter, such as KEY123 for a Figma file. It carries no scheme:// prefix. Over /gateway/mcp it arrived URL-encoded in our Teams run (19%3Aabc%40thread.tacv2). It is null when the route names no parameter, including a connector template written with single braces. |
input.context.rs_context.scopes | The matched route's scopes |
input.context.rs_context.tool_arguments | The MCP tool's arguments. For a plain HTTP caller, the JSON body, query string and path parameters merged into one object, or absent when the call carries none. |
input.context.rs_context.interface | mcp for a call through /gateway/mcp, api for a call to /gateway/<name>/... |
input.resource | One resource the token holds, which the rules are evaluated against in turn (evaluations.rego). It is not always the called connector. An MCP token that requested a connector's scopes held that connector's type in our run. A token whose grant is on urn:mcp:server holds that type instead. |
input.action.method | Never set. The HTTP method is not sent to OPA. |
input.connector | Never set |
A rule that tests input.resource.type against a connector type matches only when the token holds that type, and it raises no error when it does not. Guard on input.context.rs_context.resource_type instead, which is always the type of the resource being called.
package pbac.connectors.identos.example
import rego.v1
this_connector if input.context.rs_context.resource_type == "urn:connector:identos:example"
deny contains msg if {
this_connector
some blocked in data["pbac.operator.connectors.identos.example"].blocked_ids
input.context.rs_context.tool_arguments.item_id == blocked
msg := sprintf("example: %v is blocked by operator", [blocked])
}
Connector packages may add plain strings to deny. The aggregator wraps each one in a reason object with the code connector_deny. A package under pbac.controls must add objects.
Send feedback about this page