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.
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.