Client Credentials Flow
The Client Credentials Flow is used for machine-to-machine (M2M) communication, where no user is involved. This flow is ideal for backend services, cron jobs, and microservices.
When to use it
- A backend service accessing an API
- Scheduled tasks / cron jobs
- Microservice-to-microservice communication
- Any scenario without user interaction
Limitations
- No user context: Tokens represent the application, not a user
- No refresh tokens: When a token expires, you must request a new one
- No ID token: User identity claims are not available
Token request
curl -X POST https://your-sso-domain.com/connect/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=my-backend-service" \
-d "client_secret=my-service-secret" \
-d "scope=api"
Parameters:
| Parameter | Required | Description |
|---|---|---|
grant_type | Yes | Must be client_credentials |
client_id | Yes | Your application's client ID |
client_secret | Yes | Your application's client secret |
scope | No | The requested scope for API access (api). User scopes (openid, profile, …) cannot be used here. |
Response:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCJ9...",
"token_type": "Bearer",
"expires_in": 1800,
"scope": "api"
}
Claims in the token
Client credentials tokens contain the application's identity:
{
"sub": "my-backend-service",
"name": "My Backend Service",
"scope": "api",
"exp": 1704067200,
"iss": "https://your-sso-domain.com/"
}
The sub matches the application's client_id, and name is its display name. A client credentials token contains no user claims (e.g. email) and no role.
Best practices
- Cache tokens: Reuse tokens until they expire (check
expires_in) - Request minimal scopes: Request only the scopes you actually need
- Secure your credentials: Store the client_secret in environment variables or a secret manager
- Handle token expiration: Request a new token when the current one expires
// Example: Token caching logic
let cachedToken = null;
let tokenExpiry = null;
async function getAccessToken() {
// Return the cached token if it's still valid (with a 60s buffer)
if (cachedToken && tokenExpiry > Date.now() + 60000) {
return cachedToken;
}
// Request a new token
const response = await fetch('https://your-sso-domain.com/connect/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'grant_type=client_credentials&client_id=...&client_secret=...'
});
const data = await response.json();
cachedToken = data.access_token;
tokenExpiry = Date.now() + (data.expires_in * 1000);
return cachedToken;
}