Client Credentials Flow 🤖
The OAuth 2.0 Client Credentials flow is the standard way for a backend service to get an access token without a human user involved. Your service authenticates with its client_id and client_secret, receives a signed JWT access token, and uses it to call your protected API.
Use it for machine-to-machine (M2M) communication:
- ✅ Microservices calling each other
- ✅ Cron jobs, background workers, and daemons
- ✅ CLIs and CI/CD pipelines calling your API
- ✅ Third-party servers integrating with your platform
Do not use it in browsers or mobile apps — it requires a client_secret, which can never be shipped to a device you don’t control. For user-facing apps, use the Authorization Code flow with PKCE with our @faable/auth-js SDK.
📸 How It Works
One round trip. No redirects, no login screens, no consent prompts.
✅ Prerequisites
From the Faable Dashboard you need:
- A Machine-to-Machine Client — this gives you the
client_idandclient_secret. See Clients. - A registered API (the resource server your service will call) — its
identifieris theaudienceyou request tokens for, and it defines the permissions (scopes) available. See APIs.
🛠️ Step 1: Request an Access Token
Make a POST request to your tenant’s token endpoint:
- Endpoint: https://your-domain.auth.faable.link/oauth/token
- Method:
POST - Content-Type:
application/x-www-form-urlencodedorapplication/json— both are accepted.
Request 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. Keep it server-side only. |
audience | Recommended | The identifier of the API you want to call. Sets the token’s aud claim and its lifetime. |
scope | No | Space-separated list of permissions to request (e.g. read:users write:users). Must be defined on the target API. |
[!TIP] Instead of sending
client_idandclient_secretin the body, you can send them as an HTTP BasicAuthorizationheader (client_secret_basic), as described in RFC 6749. Both methods work identically.
Example with curl
curl --request POST \
--url 'https://your-domain.auth.faable.link/oauth/token' \
--header 'content-type: application/x-www-form-urlencoded' \
--data 'grant_type=client_credentials' \
--data 'client_id=YOUR_CLIENT_ID' \
--data 'client_secret=YOUR_CLIENT_SECRET' \
--data 'audience=https://api.example.com' \
--data 'scope=read:users'Response
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 86400,
"scope": "read:users"
}| Field | Description |
|---|---|
access_token | A signed JWT (RS256). Its aud claim matches the requested audience. |
token_type | Always Bearer. |
expires_in | Token lifetime in seconds, controlled by the target API’s token_lifetime setting (default: 86400 = 24 h). |
scope | The scopes actually granted. When the API has enforce_policies enabled, only permissions defined on the API are granted — anything else is dropped. |
[!NOTE] The Client Credentials flow never returns a
refresh_token— there is no user session to maintain. When the token expires, simply request a new one.
🚀 Step 2: Call Your API with the Token
Send the token in the Authorization header of every request to your API:
curl --request GET \
--url 'https://api.example.com/users' \
--header 'authorization: Bearer eyJhbGciOiJSUzI1NiIs...'Your API validates the token by checking its signature against your tenant’s public keys (https://your-domain.auth.faable.link/.well-known/jwks.json), its aud claim against the API’s identifier, and its scope against the permission the endpoint requires. See Validate Access Tokens for ready-to-use Express middleware, and APIs for the full model.
💻 Complete Node.js Example
A production-ready pattern: fetch the token once, cache it, and refresh it slightly before it expires.
const AUTH_DOMAIN = 'https://your-domain.auth.faable.link'
let cached: { token: string; expiresAt: number } | null = null
async function getAccessToken(): Promise<string> {
// Reuse the cached token, refreshing 60s before expiry
if (cached && cached.expiresAt > Date.now() + 60_000) {
return cached.token
}
const res = await fetch(`${AUTH_DOMAIN}/oauth/token`, {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.FAABLE_CLIENT_ID!,
client_secret: process.env.FAABLE_CLIENT_SECRET!,
audience: 'https://api.example.com',
scope: 'read:users'
})
})
if (!res.ok) {
const err = await res.json()
throw new Error(`Token request failed: ${err.error}`)
}
const { access_token, expires_in } = await res.json()
cached = { token: access_token, expiresAt: Date.now() + expires_in * 1000 }
return access_token
}
// Use it
const token = await getAccessToken()
const users = await fetch('https://api.example.com/users', {
headers: { authorization: `Bearer ${token}` }
})[!IMPORTANT] Always cache the token until it expires. Requesting a new token on every API call adds latency, and the token endpoint is rate-limited per client — a tight token loop will get throttled with
429responses.
⚠️ Common Errors
Errors follow the standard OAuth 2.0 error format (RFC 6749 §5.2): { "error": "...", "error_description": "..." }.
| Status | Cause | Fix |
|---|---|---|
400 | Missing grant_type, client_id, or client_secret. | Check all required parameters are present in the request body. |
401 | Bad credentials. | The client_secret doesn’t match. Verify it, or rotate it in the dashboard. |
404 | Client not found. | The client_id doesn’t exist in this tenant. Check the ID and the domain. |
429 | Rate limit exceeded. | You’re requesting tokens too often. Cache the token (see example above). |
🔒 Security Best Practices
- Never use this flow in a browser or mobile app. The
client_secretmust only exist on servers you control. - Store the secret in an environment variable or secret manager — never commit it to source control.
- Request only the scopes you need. Narrow tokens limit the blast radius if one leaks.
- Rotate the secret from the dashboard if you suspect it has been exposed. Rotation invalidates the old secret immediately.
- Use one client per service. Separate credentials make audit logs meaningful and let you revoke a single service without breaking the rest.
❓ FAQ
How long does the access token last?
It’s defined by the target API’s token_lifetime setting — from 60 seconds up to 30 days, with a default of 24 hours.
Does this flow return a refresh token?
No. There’s no user session to refresh. When the token expires, request a new one with the same credentials.
What’s the difference between Client Credentials and Authorization Code?
Authorization Code authenticates a user (login screen, redirects, consent). Client Credentials authenticates an application — no user, no UI, one HTTP request.
Can I use this flow to call the Faable Management API?
Yes — create an M2M client, and request a token with the Management API’s audience. This is how you automate user management, client provisioning, and more from your backend.
🔗 Related
- Clients — create an M2M client and get your credentials.
- APIs — register your API, define its audience and permissions.
- Authorization Code Flow — for user-facing applications, with the @faable/auth-js SDK.
- API Reference — full OpenAPI specification.
- RFC 6749 §4.4 — Client Credentials Grant — the official OAuth 2.0 standard.
Last updated on