How it works
PBAC separates authorization from your applications, so one policy plane governs access for your users, services, and AI agents. PBAC writes its policy decisions to an audit log.
PolicyArc is an authorization server. Clients request tokens. OPA decides what goes in them. Resource servers validate tokens via introspect. The AS writes its policy decisions to an audit log.
Where PBAC fits in your stack
Your identity providers, applications, and AI agents stay where they are. PBAC is the authorization layer in between.
Architecture
Three actors:
- Authorization Server (AS) — issues tokens, manages clients, serves policy bundles to OPA, provides the introspect and admin APIs.
- OPA — evaluates Rego policy rules against JSON policy data. The AS calls OPA during token, authorize, register, and introspect requests. A request that the AS rejects first, such as one from an unknown client or with an expired token, never reaches OPA. OPA decides; the AS enforces.
- Resource Server (RS) — any API that accepts tokens from this AS. Validates tokens by calling
/introspect. Enforces the decision and any obligations OPA returned.
This separation means you don't embed authorization logic in your applications. Your apps talk OAuth. OPA handles the rules. You change policy without touching application code.
The token lifecycle
A token goes through these steps:
- Client requests a token — via
/token(machine-to-machine) or/authorize(user-interactive). The request includes grant type, scopes, and the target resource. - AS calls OPA — OPA receives the full request context: who the client is, what it's asking for, its software statement claims, and environment data (IP, user agent).
- OPA evaluates — checks denylists, resolves client entitlements, validates grant types, evaluates per-resource rules and any custom extensions. Returns: allow/deny, granted scopes, and optional modifiers (TTL override, JIT single-use, obligations).
- AS issues the token (or rejects) — if allowed, the AS generates an opaque token, stores both the OPA input and output alongside it, and returns the token to the client.
- Client presents the token to an RS — standard Bearer token in the Authorization header.
- RS calls
/introspect— passes the token plus transaction context: the specific resource and action being requested right now. - OPA re-evaluates — same policy, but now with RS-supplied context merged in. Can narrow scopes further, add obligations, or deny based on the specific action.
- RS enforces — reads the introspect response:
active(allow/deny),scope(what's permitted), andobligations(what else to do, such as audit or mask data).
Policy is evaluated twice. Once at token issuance (can this client get a token at all? with what scopes?) and again at introspect (is this specific action on this specific resource allowed right now?). This is the key difference from systems that make one decision at login time.
For your RS integration, this means a single HTTP call (POST /introspect) gives you the full policy decision — no SDK, no library, no OPA client to maintain.
Access patterns
Machine-to-machine (client credentials)
For service accounts, background jobs, and API-to-API calls. No user involved.
User-delegated (authorization code)
For user-facing applications. User authenticates with an external Identity Provider.
OPA selects which IdP to redirect to — based on the requested resource, resource type, or ACR value. This is configured in policy data, not hardcoded.
What OPA sees
At every evaluation, OPA receives the full request context as input:
| Field | What it contains |
|---|---|
input.resources[] | What the client is requesting: resource type, resource ID, actions (scopes) |
input.subject | Who the user is: subject ID, IdP, ACR level, claims (only for user-delegated flows) |
input.context.client | The client: client_id, client_type, software_statement (all claims from DCR) |
input.context.grant_type | Which OAuth grant is being used |
input.context.environment | IP address, user agent |
input.context.rs_context | At introspect: the RS-supplied transaction context (action, resource) |
OPA evaluates this against policy data, the per-control rows such as data["pbac.operator.controls.denylist"] that Control data describes, and returns a decision.
This is the full context OPA has at decision time. Your policy can use any of it — client identity, user claims, environment, transaction context. If you can express it as a Rego rule over this input, PBAC can enforce it.
Key principles
- OPA decides, the AS enforces. The AS handles OAuth protocol mechanics. OPA handles authorization logic. They are separated so policy can change without code deployments.
- Policy evaluated twice. At token issuance and at introspect. The second evaluation can narrow, add obligations, or deny based on what the RS reports about the actual request.
- Inputs and decisions are stored. The AS stores the OPA input and output with each token it issues. Token and introspection audit entries keep the input that OPA evaluated and its result.
- Tokens are resource-bound, with limits. A token is issued for specific resource types and, optionally, specific resource servers. The gateway refuses a token anchored to one managed resource by its resource indicator when it is presented at another resource of the same type. A token issued for a whole type is accepted at every resource of that type. A token of another type is also accepted when its scope names match the scopes the call needs. See Check the scope and resource.
- Policy controls token behavior. OPA can override the token TTL, or mark a token as single-use (
jit_single_use) so it is revoked after the first introspect. The RS doesn't need to know — it just seesactive: trueoractive: false.
Next steps
- Trust & Identity — How PBAC federates with IdPs and controls who is trusted
- Clients & Access — How clients register, authenticate, and gain access
- Policy Model — How OPA decides what's allowed
- Token Lifecycle — How tokens flow from issuance through introspection
- Audit log lists what the audit log records
- Self-hosted reference architecture shows the pods, the database and the trust boundaries of one instance
Send feedback about this page