Authorization & the decision plane
Authorization & the decision plane
Cbox ID is not just an authenticator — it is the authorization decision plane for everything built on it. There is one place that answers "may this subject do X on Y?" (permissions) and "does this org have entitlement Z?" (billing-fed entitlements), and enforcement points call it rather than re-implementing logic. The model is deny-by-default and lineage-wise sits with Google Zanzibar / OpenFGA / SpiceDB: fast-changing authorization is resolved live, not frozen into a long-lived token.
┌──────────────────────────┐
"can alice │ PolicyDecisionPoint │ ← the single decision point
manage ticket:42?" │ ├─ RelationshipStore │ (deny-by-default)
──────────────────► │ │ (ReBAC / roles) │
"does org_x have │ └─ EntitlementReader │ ← cache-backed, version-fresh
entitlement pro?" │ (billing-fed) │
──────────────────► └──────────────────────────┘
Two ways to ask
1. In-process — PolicyDecisionPoint
An app that embeds the framework calls the PDP directly. Deny-by-default: the only source of "allow" is an explicit relationship grant.
use Cbox\Id\Kernel\Authorization\Contracts\PolicyDecisionPoint;
use Cbox\Id\Kernel\Authorization\ValueObjects\{Subject, ResourceRef};
$pdp = app(PolicyDecisionPoint::class);
$pdp->can('org_x', Subject::user('alice'), 'manage', ResourceRef::of('ticket', '42')); // bool
$pdp->entitlement('org_x', 'feature.sso')?->bool(); // capability gate
Relationships are stored as ReBAC tuples (RelationshipStore), supporting grants
through group membership (e.g. doc:1#viewer@group:eng#member). Entitlement reads
are served from a per-org, version-invalidated cache, so a check on every request
is cheap and a change is visible on the next read.
2. Over HTTP — POST /oauth/decisions
An external resource server presents the caller's access token and gets permissions and entitlements in one round trip. This is the hot path — resolved live, so a revoked role or a flipped capability gate takes effect on the very next call.
Entitlements here are capability gates — "this org may do X" — not billing state. Cbox ID never holds a plan name, a price, or a usage count; billing translates those into gates and pushes them in. See Entitlements & billing.
POST /oauth/decisions
Authorization: Bearer <access token>
{ "permissions": [ {"relation": "manage", "resource": "ticket:42"} ],
"entitlements": [ "feature.sso", "seats" ] }
{ "subject": {"type": "user", "id": "alice"},
"organization": "org_x",
"permissions": [ {"relation": "manage", "resource": "ticket:42", "allowed": true} ],
"entitlements": { "feature.sso": {"value": {"enabled": true}, "version": 3}, "seats": null } }
The subject and org come from introspecting the presented token (deny-by-default if
it is inactive or carries no org). A client_credentials token resolves to a
service subject; a user token to a user subject.
Freshness — why it's instant
Entitlement reads sit behind a version-tagged cache: the read key embeds the org's current version, and every write bumps that version, so the next read routes to a fresh key — atomic, correct across cache nodes, no explicit flush. A billing downgrade is therefore visible on the next decision call with no token refresh and no session disruption. Permissions are read live from the relationship store, so they are always current.
The hybrid — coarse entitlements in the token
Like WorkOS (which splits its live FGA check from RBAC/entitlement claims in the
session), Cbox ID lets you embed the coarse, slow-changing entitlements in the
access token for a stateless per-request check, while keeping the instant-critical
ones live. This is the per-entitlement EnforcementMode:
DecisionApi(default) — resolved live; instant on change; never in a token.Claims— embedded in the token as theentclaim (withent_verfor staleness detection); propagates on the next silent refresh (~15-min access-token TTL).
See Entitlements & billing
for the token shape and the CBOX_ID_EMBED_ENTITLEMENTS switch.
Rule of thumb: default everything to live (instant, no staleness surprise);
reach for Claims deliberately for the handful of gates where a zero-round-trip
check matters more than instant revocation.
Roadmap
For push-based realtime — the IdP proactively telling resource servers to
re-evaluate the moment a role, entitlement or session changes, instead of them
polling — the next step is CAEP / Shared Signals (RFC 8417 Security Event
Tokens) layered on the entitlement.updated / session-revocation events the
platform already emits.
Where to go next
- Entitlements & billing — the billing side and the hybrid in detail.
- Security — tenant isolation and the invariants underneath.
- Extending — swap the relationship store or entitlement source.