Skip to main content

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:

  1. On startup, cached_token = null.
  2. Before each API call:
    • If cached_token is null or expires in less than 30 seconds, fetch a new one and store it with the absolute expiry timestamp.
    • Use the cached token.

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:

  1. Drop the cached token.
  2. Fetch a fresh one.
  3. 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.