Machine-to-machine calls between services, jobs and partners use this grant.
When no user is involved (a nightly job calling a billing API, one microservice calling another), the client acts on its own behalf. The client credentials grant is a single back-channel request:
POST /token with grant_type=client_credentials, the requested scope, and the client's authentication.
There is no refresh token: when the access token expires, the service simply requests a new one. Cache it until shortly before expires_in runs out instead of fetching per request; many authorization servers rate-limit the token endpoint.
How the client authenticates matters:
client_secret_basic(HTTP Basic with id and secret) orclient_secret_postare simple but rely on a shared secret. Keep it in a secret manager, inject it as an environment variable, and rotate it.private_key_jwt: the client signs a short-lived JWT assertion with its private key; the AS knows only the public key. No shared secret to leak.- mTLS client authentication (RFC 8705) uses a client certificate.
- In clouds and Kubernetes, workload identity federation lets services exchange platform-issued tokens instead of holding any long-lived secret.
The resulting token's sub is the client itself, so APIs authorize based on the client's scopes or roles rather than a user.
Example
// Fetch and cache a client-credentials token (Node 22+, global fetch)
let cached = { token: null, expiresAt: 0 };
async function getServiceToken() {
if (cached.token && Date.now() < cached.expiresAt - 60_000) return cached.token;
const res = await fetch("https://auth.example.com/oauth/token", {
method: "POST",
headers: {
Authorization: "Basic " + Buffer.from(
`${process.env.CLIENT_ID}:${process.env.CLIENT_SECRET}`).toString("base64"),
},
body: new URLSearchParams({ grant_type: "client_credentials", scope: "invoices.read" }),
});
if (!res.ok) throw new Error(`token endpoint: ${res.status}`);
const { access_token, expires_in } = await res.json();
cached = { token: access_token, expiresAt: Date.now() + expires_in * 1000 };
return access_token;
}
// await fetch(url, { headers: { Authorization: `Bearer ${await getServiceToken()}` } })Remember
- Client credentials = the service acting as itself, no user
- No refresh token: cache the access token until near expiry
- Prefer private_key_jwt, mTLS or workload identity over shared secrets
Watch out for
- Requesting a new token on every API call and hitting token endpoint rate limits.