larsggu.me › Reference › OAuth 2.0 client credentials
OAuth 2.0 client credentials
POST /token grant_type=client_credentials -> access_token
The OAuth 2.0 flow for machine-to-machine access, in which the application authenticates as itself with no user present and no user consent involved.
Description
Not every integration acts for a person. A nightly reconciliation job, a data export, a service that watches a queue: these act for an organisation, and there is no browser to redirect and nobody to click consent. The client credentials grant covers that case. The application presents its own credential directly to the token endpoint and receives an access token scoped to itself.
Because there is no user in the exchange, there is also no user whose permissions bound the token. The scopes attached to the client are the only limit, which makes the registration of that client the security decision. A client registered with every scope available is a standing key to the whole account, and it is the flow's characteristic failure.
Tokens from this grant are short-lived and are not accompanied by a refresh token, since the application can simply authenticate again. That makes caching the token until shortly before expiry the expected behaviour: requesting a new token per API call is a common and avoidable source of rate-limit pressure on the token endpoint.
Client authentication itself may be a shared secret or an assertion signed with a private key. The assertion form is preferable where it is available, because the secret never leaves the client and a captured request cannot be replayed to obtain a new token.
Fields
| Field | Form | Meaning |
|---|---|---|
| grant_type | client_credentials | Names the flow. |
| client_id | string | Identifies the client. |
| client_secret | string | Shared-secret client authentication, sent over the back channel only. |
| client_assertion | signed JWT | Asymmetric alternative to the shared secret. |
| scope | space-delimited list | Bounded by what the client registration allows. |
| expires_in | seconds | Token lifetime. Cache until shortly before this elapses. |
Example
Obtaining and reusing a token
POST /token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Authorization: Basic <client_id:client_secret>
grant_type=client_credentials&scope=ledger.read+ledger.export
HTTP/1.1 200 OK
{"access_token":"at_...","token_type":"Bearer","expires_in":3600,
"scope":"ledger.read ledger.export"}
GET /v1/ledger/entries HTTP/1.1
Authorization: Bearer at_...One token is fetched, then reused for every request until it approaches expiry.
Failure modes
- Requesting a token per request instead of caching it, which loads the token endpoint far more than the API itself.
- Registering the client with the full scope list because narrowing it later seems harder.
- Holding the client secret in a build artefact rather than in the environment, where it travels wherever the artefact travels.
- Treating expiry as an error to alert on rather than a routine refresh.
Related entries
Topic: Identity. Last modified 2026-09-06.