Skip to main content
Version: Latest

Token Lifecycle

A PBAC token is not a static credential. Policy is evaluated when the token is issued, and evaluated again when the token is used. The second evaluation can change the outcome — narrowing scopes, adding obligations, or denying access — based on what the resource server reports about the actual request.

This document traces a token from its first request through to expiry, explaining what happens at each stage and why.


The two-evaluation model

This is the central concept in PBAC token design. Most OAuth systems make one authorization decision — at login or token issuance — and then trust that decision for the token's lifetime. PBAC makes two:

Evaluation 1 — Issuance. When a client requests a token, OPA evaluates whether to grant it and with what scopes. This answers: can this client get a token at all, for this resource type, with these scopes?

Evaluation 2 — Introspection. When the resource server presents the token to the AS, OPA evaluates again — this time with the RS-supplied transaction context. This answers: is this specific action on this specific resource permitted right now, given the full request context?

The second evaluation has access to everything the first had, plus what the RS knows about the actual request. It can return a narrower scope set than was originally granted, add obligations the RS must enforce, or deny access entirely.

This is what makes PBAC different from static token systems. A token is not a pre-computed decision — it's a reference that triggers a fresh policy evaluation every time it's used.


Issuance

All token requests go to POST /token. The grant type determines what context is available to OPA.

Grant types:

Grant typeUse caseSubject context
client_credentialsMachine-to-machineNone — client identity only
authorization_codeUser-interactiveUser identity from IdP
refresh_tokenLong-lived sessionsCarries original user context
urn:ietf:params:oauth:grant-type:token-exchangeService delegationOriginal subject + acting client

For every grant type, the flow is:

  1. Client authenticates (client secret, private key JWT, or DPoP proof)
  2. AS validates the request parameters and client credentials
  3. AS calls OPA with the full request context — client identity, software statement claims, requested resource, scopes, grant type, environment
  4. OPA returns: allow/deny, granted scopes, and optional modifiers (TTL override, JIT single-use flag, obligations for the issuance event)
  5. If allowed: AS generates an opaque token, stores the OPA input and output alongside it (pdpInput, pdpOutput), returns the token to the client

The stored pdpOutput is what makes introspection work: the AS doesn't re-examine the original request — it uses the stored OPA output as the baseline for the second evaluation.

Token response (client credentials):

{
"access_token": "eyJhb...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read"
}

Introspection (re-evaluation)

When the resource server receives a client's request, it calls POST /introspect with the token and RS-supplied context (resource, scopes). OPA re-evaluates with the full transaction context and returns an authoritative, current decision.

Key behaviors:

  • Scope narrowing — the introspect response may return fewer scopes than originally granted, based on the specific resource being accessed
  • Obligations — OPA can attach RS-enforceable obligations (audit, masking, rate limiting)
  • Denial — OPA can deny at introspect time even if the token was originally allowed

JIT single-use tokens

OPA can set jit_single_use: true in the token output. When this flag is present:

  • The token is valid on first introspection — active: true is returned normally
  • The token is immediately consumed — any subsequent introspection returns active: false

This is designed for operations that must not be replayable. The primary use case is AI agent workflows: an agent receives a one-time token to perform a specific action (write a record, trigger a transaction), and the token is invalid after that action completes. Even if the token is intercepted, it cannot be reused.

JIT single-use is configured in Rego policy extensions, typically based on client claims:

{
"pdpOutput": {
"jit_single_use": true,
"granted_scopes": ["write"]
}
}

Token expiry and TTL

Tokens expire based on TTL. The default TTL is set in AS configuration. OPA can override it per-request via expires_in_override in the token output — for example, issuing shorter-lived tokens to lower-trust clients.

Refresh tokens. For user-delegated flows with refresh_token in the client's grant_types, the AS issues a refresh token alongside the access token. Refreshing runs OPA policy again — so if a user's entitlements have changed since the original grant (e.g., their access level was reduced), the new access token reflects current policy, not the original grant.


DPoP (sender-constrained tokens)

By default, tokens are bearer tokens — anyone who holds the token can use it. DPoP (Demonstrating Proof of Possession, RFC 9449) changes this by binding the token to the client's private key.

A DPoP-bound token carries a cnf.jkt claim — the JWK thumbprint of the client's public key:

{
"active": true,
"token_type": "DPoP",
"cnf": {
"jkt": "0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I"
},
"scope": "read"
}

The RS must verify that the DPoP proof header on the incoming API request was signed by the private key corresponding to this thumbprint. A stolen token without the private key cannot produce a valid proof and is useless.

DPoP is registered per-client (dpop_bound_access_tokens: true) and applies to every token request from that client. For public clients (SPAs, mobile apps), DPoP also constrains refresh tokens — the same key used to obtain the refresh token must be used to redeem it.


Delegation

Tokens can represent delegated authority — access granted by one principal on behalf of another. PBAC supports three delegation mechanisms:

Authorization code flow — the user delegates to a client by authenticating. The resulting token carries the user's sub (who authenticated) and the client's client_id (who is acting). This is the standard user-facing delegation model.

Token exchange (RFC 8693) — an agent or downstream service exchanges a token for a new one scoped to a specific resource. The resulting token carries sub (the original user) and an act claim (the acting service). Each exchange can narrow scope — an agent token can have fewer permissions than the user token it was derived from, never more. This chain of act claims is auditable.

UMA delegation — a resource owner explicitly grants another user or client access to specific resources. These grants are stored in the AS. At introspect time, OPA checks them and can allow access that the client's own software statement wouldn't cover — because the resource owner explicitly authorized it.

From the RS's perspective, all three produce the same thing: a token to introspect. The RS calls /introspect, reads active, scope, and obligations, and enforces. The delegation mechanism is transparent to the RS.


Next steps

  • Clients & Access — How clients register and what they can request
  • Policy Model — How OPA evaluates the request context behind every token decision
  • Architecture — Where the token lifecycle fits in the overall system