Skip to main content

Client Credentials Flow

The Client Credentials flow is used for machine-to-machine (M2M) authentication where no user is involved. The client authenticates directly with its credentials to obtain an access token.

When to Use

  • Backend services calling other backend services
  • Automated scripts and cron jobs
  • CI/CD pipelines
  • System integrations
  • Any scenario where a service (not a user) needs to authenticate

Flow Diagram

┌─────────────┐                              ┌──────────┐
│ Your Service │ │ S-Auth │
└──────┬──────┘ └────┬─────┘
│ │
│ 1. POST /token │
│ client_id + client_secret │
│ ────────────────────────────────────────>│
│ │
│ 2. Validate credentials │
│ Generate access token │
│ │
│ 3. Return access token │
│ <────────────────────────────────────────│
│ │
│ 4. Use token to access resources │
│ ────────────────────────────────────────>│
│ │

Token Request

curl -X POST https://auth.sebbyk.net/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "Authorization: Basic BASE64(client_id:client_secret)" \
-d "grant_type=client_credentials" \
-d "scope=api:read"

Request Parameters

ParameterRequiredDescription
grant_typeYesMust be client_credentials
scopeNoSpace-separated list of requested scopes

Authentication

The client must authenticate using one of:

Client Secret Basic (Recommended):

Authorization: Basic BASE64(client_id:client_secret)

Client Secret Post:

client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET

Token Response

{
"access_token": "sat_abc123...",
"token_type": "Bearer",
"expires_in": 900,
"scope": "api:read"
}

Note: Client credentials grants do not receive refresh tokens since the client can always re-authenticate.

Client Configuration

To use the client credentials flow, your OAuth client must be configured with:

  1. Confidential client type - Can securely store secrets
  2. client_credentials in allowed_grants - Explicitly enabled in admin dashboard

Example: Node.js Service

class AuthClient {
private clientId: string;
private clientSecret: string;
private tokenUrl: string;
private accessToken: string | null = null;
private tokenExpiry: number = 0;

constructor() {
this.clientId = process.env.CLIENT_ID!;
this.clientSecret = process.env.CLIENT_SECRET!;
this.tokenUrl = 'https://auth.sebbyk.net/token';
}

async getAccessToken(): Promise<string> {
// Return cached token if still valid (with 60s buffer)
if (this.accessToken && Date.now() < this.tokenExpiry - 60000) {
return this.accessToken;
}

// Request new token
const response = await fetch(this.tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Basic ${Buffer.from(
`${this.clientId}:${this.clientSecret}`
).toString('base64')}`,
},
body: new URLSearchParams({
grant_type: 'client_credentials',
}),
});

if (!response.ok) {
throw new Error(`Token request failed: ${response.status}`);
}

const data = await response.json();

this.accessToken = data.access_token;
this.tokenExpiry = Date.now() + (data.expires_in * 1000);

return this.accessToken;
}

async makeAuthenticatedRequest(url: string, options: RequestInit = {}) {
const token = await this.getAccessToken();

return fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`,
},
});
}
}

// Usage
const authClient = new AuthClient();

async function callProtectedApi() {
const response = await authClient.makeAuthenticatedRequest(
'https://api.example.com/data'
);
return response.json();
}

Example: Python Service

import requests
import time
from base64 import b64encode

class AuthClient:
def __init__(self, client_id: str, client_secret: str):
self.client_id = client_id
self.client_secret = client_secret
self.token_url = 'https://auth.sebbyk.net/token'
self.access_token = None
self.token_expiry = 0

def get_access_token(self) -> str:
# Return cached token if still valid (with 60s buffer)
if self.access_token and time.time() < self.token_expiry - 60:
return self.access_token

# Request new token
credentials = b64encode(
f'{self.client_id}:{self.client_secret}'.encode()
).decode()

response = requests.post(
self.token_url,
headers={
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': f'Basic {credentials}',
},
data={'grant_type': 'client_credentials'},
)
response.raise_for_status()

data = response.json()
self.access_token = data['access_token']
self.token_expiry = time.time() + data['expires_in']

return self.access_token

def make_authenticated_request(self, url: str, **kwargs):
token = self.get_access_token()
headers = kwargs.pop('headers', {})
headers['Authorization'] = f'Bearer {token}'
return requests.request(url=url, headers=headers, **kwargs)


# Usage
auth_client = AuthClient(
client_id='your-client-id',
client_secret='your-client-secret'
)

response = auth_client.make_authenticated_request(
'https://api.example.com/data',
method='GET'
)

Error Responses

Invalid Client

{
"error": "invalid_client",
"error_description": "Client authentication failed"
}

Unauthorized Grant Type

{
"error": "unauthorized_client",
"error_description": "Client not authorized for this grant type"
}

Invalid Scope

{
"error": "invalid_scope",
"error_description": "Requested scope is not allowed"
}

Security Considerations

  1. Protect client secrets - Store in environment variables or secrets manager
  2. Use HTTPS only - Never send credentials over unencrypted connections
  3. Rotate secrets regularly - Regenerate client secrets periodically
  4. Principle of least privilege - Only request the scopes you need
  5. Token caching - Cache tokens to reduce load on the auth server
  6. No user context - Tokens from this flow have no associated user