Skip to content

Access token reference

Access token reference

Access tokens are JWTs (RFC 9068) signed by the environment's active key and published at /.well-known/jwks.json. The JOSE header carries typ: at+jwt, so a resource server can refuse an ID Token presented in its place. Every grant — authorization code, refresh, client credentials, device, CIBA and token exchange — mints through the same TokenIssuer, so the shape below does not depend on how the token was obtained.

Claims

Claim Present Value
iss always The environment's issuer URL (matches discovery).
sub always The user's subject id; for client_credentials, the client id.
client_id always The client the token was issued to.
aud always See Audience.
scope always Space-separated granted scopes (may be empty).
jti always Unique id; recorded so the token can be revoked and introspected.
iat, exp always Issued-at and expiry. Lifetime is the client's access_token_ttl, else cbox-id.oauth.access_token_ttl (900 s); a token whose scopes exactly match a cbox-id.oauth.scope_token_ttls entry (by default account:apps:read, 12 h) gets that entry's lifetime instead. Always capped by max_access_token_ttl.
org always The organization the grant is bound to, or null.
org_name when org is set The organization's display name.
roles, permissions user tokens with any grant The person's roles and their permissions for one app — the API's linked app when the token is for a registered API that names one, otherwise the requesting client's. Environment-wide grants count when no organization is bound. Absent on client_credentials.
feature_flags with the feature_flags scope The sorted keys of every feature flag on for the subject in org; an empty list when none is. A client_credentials token is evaluated for its organization.
ent, ent_ver when the org has Claims-mode entitlements Embedded capability gates and the highest version among them.
cnf.jkt DPoP-bound tokens RFC 9449 key thumbprint; token_type is then DPoP.
auth_time, acr user grants with a recorded login RFC 9470 §6.1: when the person signed in, and the class reached (urn:cbox-id:aal1/aal2, derived from the login's amr). Authorization code and its refreshes carry both, always the original login's values. CIBA carries auth_time only. Device, token exchange, support-session and client_credentials tokens carry neither. See Require step-up authentication.
custom when a TokenMinting hook adds them Hooks can add claims but never overwrite the ones above.

Audience

aud is decided once per token by the audience resolver:

Situation aud
No resource, no registered API scope the issuer (RFC 9068 §2.2 requires an audience)
resource that is not a registered API that URI, verbatim; registered API scopes are dropped from the token
resource naming a registered API, or no resource and the scopes belong to exactly one API the API's identifier — or [identifier, issuer] when the token carries openid, so UserInfo still accepts it
No resource and scopes of more than one API refused: invalid_target

When a token is for a registered API its scope holds only that API's scopes plus the protocol scopes (openid, profile, email, offline_access, organizations, groups, feature_flags). The rules and the ownership model are in APIs and scopes.

A refresh re-mints the scopes and audience the original token was granted, never more.

Verifying one

A resource server should check iss, the signature against the JWKS, typ: at+jwt, exp, that its own identifier is in aud, and the scopes or permissions it needs. A string aud and an array aud are both valid JWT; compare by membership.

Every token an app holds carries that app's client_id, including one issued to a person signed in through it. Do not treat client_id as the caller's identity on its own. Only a machine token (client_credentials) acts as the app: active, sub equal to client_id, and no act claim. Introspection::isMachine() checks all three:

if ($token->isMachine()) {
    // The app itself: its own secrets, its own grants, service-to-service questions.
} else {
    // A person ($token->subject), through that app. Hold them to their own access.
}

Resource-server introspection

A confidential client can introspect its own tokens at /oauth/introspect. A resource server accepting tokens minted to another client needs an explicit host-side grant:

// config/cbox-id.php, within oauth.introspection:
'resource_servers' => [
    'registered-resource-client-id' => ['https://api.example.com'],
],

The default is an empty map. Register the API audience and confidential client normally; then an operator grants only the exact audience that server owns. API-to-app RBAC linkage (oauth_apis.client_id) does not grant introspection access. This map is not a dynamic client-registration field. No wildcard, URI-prefix, issuer-audience or multi-audience matching is performed.

The caller authenticates using its own confidential-client credentials and sends the original access token as token. A valid grant returns the normal introspection response, including the original client_id, subject, audience, token type and authentication context. The resource server must still check its audience, required scopes, caller integration delegation and current application permissions. A DPoP token's cnf must be enforced; an introspection grant does not convert it into an ordinary bearer.

Cross-client introspection also checks current organization status and, for user tokens bound to an organization, active membership. Removed or suspended members and inactive organizations return only {"active": false}. Organization-owned resource clients cannot inspect another organization's token. Environment scoping and token revocation remain in force. Removing a configured grant stops subsequent cross-client introspection without revoking the original client's token. Own-token introspection behavior is unchanged.

The ResourceIntrospectionAccess contract can be rebound by hosts that keep these explicit grants in another authoritative store.