Authentication
Persium uses the OAuth 2.0 Client Credentials flow. Your backend
exchanges its CLIENT_ID and CLIENT_SECRET at the Keycloak token
endpoint for a short-lived bearer token, and sends that token on every
API call.
There is no user, no consent screen, and no refresh token — when the access token expires, you simply call the token endpoint again.
Fetching a token
curl -s -X POST "$TOKEN_URL" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=$CLIENT_ID" \
-d "client_secret=$CLIENT_SECRET"
Response:
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"expires_in": 300,
"token_type": "Bearer",
"scope": "email profile"
}
Then on every API call:
curl -s "$API_BASE_URL/me" \
-H "Authorization: Bearer $ACCESS_TOKEN"
Token lifetime
Tokens last 5 minutes by default (expires_in: 300). Do not
hardcode this — read expires_in from each response so you stay
correct if Persium adjusts it.
Caching guidance
Fetching a token per API call will rate-limit you at Keycloak. Use an in-memory cache:
- On startup,
cached_token = null. - Before each API call:
- If
cached_tokenisnullor expires in less than 30 seconds, fetch a new one and store it with the absolute expiry timestamp. - Use the cached token.
- If
That's it. No refresh tokens, no rotation jobs, no background refresh loops needed.
:::tip Worker / pod count matters If you run many replicas, each replica caches independently — that's fine, Keycloak can handle one token request per process every five minutes. Don't try to share tokens between replicas via Redis; the extra complexity isn't worth it. :::
Token failures
A 401 Unauthorized from any API endpoint means the token is missing,
expired, malformed, or unknown to Keycloak. The right reaction is:
- Drop the cached token.
- Fetch a fresh one.
- Retry the original request once.
If the fresh token also gets 401, stop — see Troubleshooting.
Calling identity
GET /me returns the identifying values for the calling client.
Useful as a CI/CD smoke test:
{
"client_id": "acme-corp-backend",
"organisation_uuid": "8d2f1a3e-1234-4c1b-9999-abcdefabcdef",
"label": "Acme Corp production backend",
"all_organisations": false
}
The all_organisations field is reserved for Persium-issued elevated
clients with cross-organisation access. It is always false for
customer clients.