Skip to Content
🔐 Faable AuthOAuth 2.0 FlowsClient Credentials (M2M)

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:

  1. A Machine-to-Machine Client — this gives you the client_id and client_secret. See Clients.
  2. A registered API (the resource server your service will call) — its identifier is the audience you 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-urlencoded or application/json — both are accepted.

Request Parameters

ParameterRequiredDescription
grant_typeYesMust be client_credentials.
client_idYesYour application’s Client ID.
client_secretYesYour application’s Client Secret. Keep it server-side only.
audienceRecommendedThe identifier of the API you want to call. Sets the token’s aud claim and its lifetime.
scopeNoSpace-separated list of permissions to request (e.g. read:users write:users). Must be defined on the target API.

[!TIP] Instead of sending client_id and client_secret in the body, you can send them as an HTTP Basic Authorization header (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" }
FieldDescription
access_tokenA signed JWT (RS256). Its aud claim matches the requested audience.
token_typeAlways Bearer.
expires_inToken lifetime in seconds, controlled by the target API’s token_lifetime setting (default: 86400 = 24 h).
scopeThe 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 429 responses.


⚠️ Common Errors

Errors follow the standard OAuth 2.0 error format (RFC 6749 §5.2): { "error": "...", "error_description": "..." }.

StatusCauseFix
400Missing grant_type, client_id, or client_secret.Check all required parameters are present in the request body.
401Bad credentials.The client_secret doesn’t match. Verify it, or rotate it in the dashboard.
404Client not found.The client_id doesn’t exist in this tenant. Check the ID and the domain.
429Rate 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_secret must 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.


Last updated on