Platform token returns 403 with "requires admin:read or admin:write scope"
Symptom#
Your service obtains a token via client_credentials at /api/v1/platform/oauth/token and the call succeeds. But when the service uses the token to call an admin endpoint (/api/v1/admin/...), it gets:
1 2 | |
Cause#
The admin API gates every endpoint on the JWT carrying admin:read or admin:write in its scope claim. A platform token's scopes come from one of two places:
- The
scopeform parameter you sent on theclient_credentialsrequest, or - If you sent none, the application's
allowed_scopescolumn (the registered scope superset).
If neither path put admin:read (or admin:write) into the token, the admin API rejects.
Fix#
Two things have to be true:
1. The application's allowed_scopes must include the admin scope#
Check your application's row in the admin UI (Applications → your app → Settings → Allowed Scopes). If admin:read isn't ticked, the application can never produce a token with that scope, regardless of what you ask for.
If the admin UI's scope picker doesn't show admin:read as a togglable option, the picker is limited to its hardcoded list — bump the application via API:
1 2 3 4 | |
(Include any other scopes the app already needs — allowed_scopes is a replacement, not a merge.)
2. The token request must carry that scope#
Either include it explicitly:
1 2 3 4 | |
Or omit the scope parameter entirely so the token inherits the full allowed_scopes list.
Verify#
Decode the access token and confirm admin:read is in scope:
1 | |
The scope claim should be a space-separated string containing admin:read (and/or admin:write).
Gotcha: stale cached tokens#
Bumping the application's allowed_scopes does not retroactively widen existing tokens. Issued JWTs are immutable — they carry whatever scopes they had at mint time. After changing the app's scopes, force your service to request a fresh token (clear any in-memory cache).
Related#
- Concepts → Tokens and scopes — the full scope list.
- Concepts → Applications — application configuration including
allowed_scopes.