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
| Parameter | Required | Description |
|---|---|---|
grant_type | Yes | Must be client_credentials |
scope | No | Space-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:
- Confidential client type - Can securely store secrets
client_credentialsin 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
- Protect client secrets - Store in environment variables or secrets manager
- Use HTTPS only - Never send credentials over unencrypted connections
- Rotate secrets regularly - Regenerate client secrets periodically
- Principle of least privilege - Only request the scopes you need
- Token caching - Cache tokens to reduce load on the auth server
- No user context - Tokens from this flow have no associated user