Client credentials for services

Get an access token for a service, job or daemon that calls an API without a user, and reuse it until it is about to expire.

View as Markdown

When a program calls an API on its own behalf, with no user involved, it uses the client credentials grant. The program authenticates to OneiD with its client ID and secret and receives an access token. This guide shows the request, the response, what the token contains and how to cache it.

Before you start

Ask your OneiD administrator for a client with:

  • client type confidential
  • the client credentials grant
  • the API scopes the service needs, for example orders.read

You receive a client ID and a client secret. Public clients cannot use client credentials.

Warning The client secret is a password for your service. Keep it in a secret store or an environment variable supplied at run time. Never commit it to source control or write it to logs.

Request a token

Send a POST to the token endpoint with grant_type=client_credentials and the scopes you need.

The client ID and secret go in the Authorization header, joined by a colon and Base64-encoded.

POST /connect/token HTTP/1.1
Host: YOUR_ONEID
Authorization: Basic WU9VUl9DTElFTlRfSUQ6WU9VUl9DTElFTlRfU0VDUkVU
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&scope=orders.read
curl -s https://YOUR_ONEID/connect/token \
  -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
  -d grant_type=client_credentials \
  -d scope=orders.read

With client_secret_post

The client ID and secret go in the form body. OneiD accepts this, but prefer client_secret_basic, because a body is more likely to be logged by tools along the way.

POST /connect/token HTTP/1.1
Host: YOUR_ONEID
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&scope=orders.read&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET
curl -s https://YOUR_ONEID/connect/token \
  -d grant_type=client_credentials \
  -d scope=orders.read \
  -d client_id=YOUR_CLIENT_ID \
  -d client_secret=YOUR_CLIENT_SECRET

To request several scopes, separate them with spaces: scope=orders.read orders.write (URL-encoded as orders.read%20orders.write). A scope that the client is not allowed returns invalid_scope.

Not supported OneiD authenticates clients with a client secret only. private_key_jwt and mTLS client authentication cannot be used.

The response

{
  "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkVYQU1QTEVfS0lEIn0.EXAMPLE_ACCESS_TOKEN_PAYLOAD.EXAMPLE_SIGNATURE",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "orders.read"
}

There is no ID token, because no user signed in, and no refresh token. When the access token is about to expire, request a new one with the same call.

expires_in is in seconds. Access tokens last 1 hour by default; your administrator can change this for the client.

Checkpoint The curl command prints JSON with an access_token and "token_type": "Bearer". If you see an error instead, check the client ID and secret, then ask your administrator to confirm that the client is enabled, is confidential and has the client credentials grant and the scope you requested.

What the token contains

The access token is a JWT signed with RS256. A decoded payload looks like this:

{
  "iss": "https://YOUR_ONEID/",
  "sub": "YOUR_CLIENT_ID",
  "client_id": "YOUR_CLIENT_ID",
  "name": "Orders sync job",
  "scope": "orders.read",
  "iat": 1767225600,
  "exp": 1767229200
}
  • sub is the client ID. There is no user.
  • name is the client’s display name, as registered by the administrator.
  • scope lists the granted scopes, separated by spaces.
  • There is no aud claim. An API checks the issuer, signature, expiry and scope instead.

An API can tell a service token from a user token by comparing sub with client_id, or by the scopes it requires. See Protect an API.

Call the API

GET /orders HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkVYQU1QTEVfS0lEIn0.EXAMPLE_ACCESS_TOKEN_PAYLOAD.EXAMPLE_SIGNATURE

Cache the token

Request a token once and reuse it for every call until it is close to expiry. Requesting a new token for each API call slows your service down and can hit OneiD’s rate limits.

A simple pattern:

  1. On the first call, request a token and remember it with its expiry time: now plus expires_in.
  2. Before each API call, reuse the cached token if it has more than a minute left.
  3. Otherwise request a new one. If several threads need a token at the same moment, let one request it and the others wait for the result.
  4. If the API answers 401, discard the cached token, request a new one and retry once.
let cached = null;

async function getToken() {
  if (cached && cached.expiresAt - Date.now() > 60_000) return cached.token;

  const res = await fetch('https://YOUR_ONEID/connect/token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      Authorization: 'Basic ' + Buffer.from(`${process.env.CLIENT_ID}:${process.env.CLIENT_SECRET}`).toString('base64'),
    },
    body: new URLSearchParams({ grant_type: 'client_credentials', scope: 'orders.read' }),
  });
  if (!res.ok) throw new Error(`Token request failed: ${res.status}`);

  const body = await res.json();
  cached = { token: body.access_token, expiresAt: Date.now() + body.expires_in * 1000 };
  return cached.token;
}

Most OAuth client libraries, and many HTTP client frameworks, cache client credentials tokens for you. Check that yours does before writing your own.

Handle rate limiting

The token endpoint is rate limited. When a service sends too many requests, OneiD answers HTTP 429:

HTTP/1.1 429 Too Many Requests
Retry-After: 12
Content-Type: application/json

{"error":"slow_down","error_description":"Too many requests. Try again in 12 seconds."}

Wait for the number of seconds in Retry-After before trying again. Do not retry in a tight loop. Caching tokens as shown above keeps a service well clear of the limits. See Rate limits.

When the secret changes or expires

If the client secret has expired, OneiD returns invalid_client with the description “The client secret has expired.” If your administrator disables the secret or the client, existing tokens are revoked and new requests fail. Read the secret from configuration at start-up so you can replace it without a code change.

Learn more