Skip to main content

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.

EndpointOPA pathRuns custom controls
/token/v1/data/oauth/tokenYes
/introspect/v1/data/oauth/token, with the token's grant and the resource server's context addedYes
/authorize/v1/data/oauth/authorizeYes
/register/v1/data/oauth/registerNo. Its hook is the shipped package oauth.register.ext.
/access/v1/evaluation (AuthZEN)/v1/data/direct/testNo. 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.

FieldContents
input.resource{type, id, properties} of one requested resource
input.actionOne requested scope, as a string. Read by deny and rar_allow.
input.actionsEvery requested scope for the resource. Read by rs_obligations and subject_obligations.
input.subjectThe user, or null for a client credentials token
input.contextThe 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}
}
FieldContents
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.
subjectnull for client credentials. For a user, sub, idp, acr and session_id, plus claims the identity provider passed.
context.client.client_idThe client making the request
context.client.client_typeconfidential or public
context.client.registration_typeHow the client registered. We saw DCR_PLAIN for a client registered without a statement.
context.client.software_statementEvery claim of the client's software statement, or {}
context.entrypointtoken
context.grant_typeThe grant, such as client_credentials or urn:ietf:params:oauth:grant-type:token-exchange
context.environmentThe caller's ip and user_agent as the server saw them
context.rs_contextOnly at introspection. The context the resource server sent with the call.
context.token_granted_resourcesOnly at introspection. What the token was granted at issuance.
context.subject_token_granted_resourcesOnly in token exchange. What the subject token was granted.
context.actorOnly in token exchange with an actor_token. The actor's client_id, and its sub when it has one.
context.delegationsDelegation grants for the subject and resource, when there are any
dpopWhether 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"]
}
FieldContents
allowtrue when a token is issued
reasonOne reason for a refused request, such as all_resources_denied, client_denied or grant_type_not_allowed. null when allowed.
deny_reasonsEvery reason a control added, for allowed and refused requests alike
granted_resourcesEach granted resource with its granted_actions
subject_obligationsSteps the user must complete, such as approval, before the token is issued
rs_obligationsObligations for the resource server. Only filled at introspection.
expires_in_overrideThe token lifetime in seconds when a control set one. The shortest wins.
jit_single_usetrue when a control made the token single use
matched_segmentsThe 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.

FieldOn a gateway call
input.context.rs_context.resource_typeThe connector's resource type. Use it to scope a rule to one connector.
input.context.rs_context.resource_nameThe managed resource's name
input.context.rs_context.resource_idThe 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.scopesThe matched route's scopes
input.context.rs_context.tool_argumentsThe 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.interfacemcp for a call through /gateway/mcp, api for a call to /gateway/<name>/...
input.resourceOne 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.methodNever set. The HTTP method is not sent to OPA.
input.connectorNever 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.