Tokens and scopes
Token format#
Every token ScaiKey issues is a signed JWT. Signing algorithm is RS256 by default; the platform can be configured for ES256. Public keys are published at the JWKS endpoint and rotated on a schedule.
Three token shapes:
- Access token — bearer credential for API calls. Carries
sub,aud,scope, and a few platform-specific claims. - ID token — OIDC user-identity assertion. Issued when
openidis in the requested scope. Contains identity claims (email,name,groups, etc.) keyed off the requested scopes. - Refresh token — opaque (not a JWT), one-time-use, hash-stored in the database. Exchanged for a fresh access token at the token endpoint.
Access token claims#
A standard ScaiKey access token looks like this:
1 2 3 4 5 6 7 8 9 | |
Additional claims depending on grant and app type:
| Claim | When set | Meaning |
|---|---|---|
tenant_id |
Tenant-scoped flows | The tenant the principal belongs to |
partner_id |
When the user has a partner role | Set on partner_admin and on partner-level tokens |
role |
Admin tokens | super_admin / partner_admin / tenant_admin |
client_id |
All flows | The OAuth client_id that requested the token |
app_scope |
Platform tokens | GLOBAL / PARTNER / TENANT — the app's scope level |
platform_token |
Set true on client_credentials tokens minted by GLOBAL apps |
Marker for the admin API to grant cross-tenant access |
token_type (claim) |
client_credentials grant |
"client_credentials" — for consumers that distinguish service vs user tokens |
act |
Tokens minted via Token Exchange | RFC 8693 delegation chain (who exchanged on whose behalf) |
Note on token_type: the OAuth response envelope also has a field called token_type, which is always "Bearer" (it describes how the token should be used, not what kind of token it is). These are different things at different layers — the response field is RFC 6749 §5.1; the JWT claim is a ScaiKey extension.
Scopes#
Standard OIDC scopes#
| Scope | Claims released |
|---|---|
openid |
Required for OIDC. Triggers ID token issuance. |
profile |
name, given_name, family_name, preferred_username, picture, locale, zoneinfo |
email |
email, email_verified |
groups |
groups — array of group slugs the user belongs to |
offline_access |
Requests a refresh token |
ScaiKey admin scopes#
Used by service integrations that call ScaiKey's own admin API. Granted only to applications whose allowed_scopes list includes them.
| Scope | Grants |
|---|---|
admin:read |
Read access to the entire admin API (partners, tenants, users, groups, apps, sessions, audit) |
admin:write |
Write access to the admin API |
users:read, groups:read |
Narrower read-only scopes; useful when you want a service to mirror just one resource type |
scaicore:view, scaicore:manage |
Reserved for ScaiCore-tier operations |
These are platform-token scopes — they only do anything meaningful on JWTs minted via client_credentials for GLOBAL apps. A user JWT doesn't grant admin access just because the scope is present; the user must also have the appropriate role.
How scopes are constrained#
The token endpoint never grants more than three things permit, simultaneously:
- What the application's
allowed_scopescolumn lists (registered). - What the request asked for (
scope=...parameter). - For Token Exchange: what the subject token already had (
subject_scopes).
If you ask for a scope your app isn't registered for, the token comes back narrower (or, in Token Exchange, with no overlap → invalid_scope error).
Token lifetimes#
| Token | Default | Where it's set |
|---|---|---|
| Access token | 3600 s (1 h) | applications.token_lifetime on the audience app — configurable per application |
| Refresh token | 30 days | applications.refresh_token_lifetime |
| Authorization code | 600 s (10 min) | Platform setting (OIDC.authorization_code_ttl) |
| ID token | Same as access | Always matches the access token's exp |
A super_admin can change token_lifetime per application via PATCH /api/v1/admin/applications/{id}. The change applies to the next token minted; existing tokens keep their original exp.
For Token Exchange: the exchanged token's lifetime is the target's token_lifetime, not the subject's. A long-running async worker can therefore exchange a near-expiry user token for a fresh full-lifetime audience-restricted token.
Revocation semantics#
ScaiKey access tokens are stateless JWTs. The platform does not maintain a per-token revocation list, and resource servers verify tokens locally against the published JWKS — there's no round-trip to ScaiKey on every API call. The consequence is that admin actions which "revoke" access don't immediately invalidate in-flight access tokens.
| Admin action | When the change takes effect on already-issued access tokens |
|---|---|
Suspending a user (users.status = SUSPENDED) |
At the access token's next refresh, or its exp, whichever comes first |
| Deleting / soft-deleting a user | Same — next refresh or exp |
| Removing a user from a group | Same — the groups claim is re-evaluated at refresh |
Revoking an admin role (admin_roles.is_active = 0) |
Immediately for ScaiKey's own admin API (require_admin does a live lookup); resource servers reading their own claims see it at refresh |
Revoking a refresh token (POST /token/revoke) |
The associated refresh token chain dies immediately — but any access token already issued from it stays valid until exp |
Disabling an application (is_active = false) |
New token requests fail; existing access tokens already in flight stay valid until exp |
| Removing partner_id / allowed_partner_ids | Next mint reflects the change; in-flight tokens carry the stale partner_ids claim until exp |
The upper-bound staleness window equals the audience application's token_lifetime (default 1 h, configurable down to 5 min for security-sensitive resource servers, or up to 24 h for compute-heavy workloads where re-auth overhead matters).
For workflows that need stronger revocation guarantees:
- Reduce
token_lifetimeon the resource server's application row (PATCH /api/v1/admin/applications/{id}withtoken_lifetime). Trade-off: more JWKS verification load on the resource server, more refresh round-trips on the client. - Implement token introspection on the resource server side via
POST /api/v1/oauth/introspect(RFC 7662). Every API call costs one ScaiKey round-trip; in exchange, revocation is instant. Only sensible for low-traffic, high-value endpoints. - Subscribe to the relevant webhooks (
user.suspended,user.deleted,group.member_removed,admin_role.revoked) and invalidate your own session/cache layer in response. Keeps token validation fast and revocation timely.
Verifying tokens#
Standard JWT verification using the public key published at the JWKS endpoint. Steps:
- Fetch JWKS from
$SCAIKEY/api/v1/platform/.well-known/jwks.json(or the tenant equivalent). - Read the
kidheader from the JWT to pick the right key. - Verify signature.
- Validate
iss,aud,exp,nbf(if present). - Apply scope checks based on your endpoint's policy.
Don't cache the JWKS forever; refresh periodically or on any kid you don't recognize. Keys are rotated occasionally; old keys remain in the JWKS during their grace period so in-flight tokens still validate.