Client Credentials Flow (M2M)
The Client Credentials flow is used for server-to-server (machine-to-machine or M2M) authentication, where there is no user interaction. This is ideal for background services, microservices, daemons, and AI agents.
When to Use Client Credentials Flow
Use the Client Credentials flow when:
- ✅ No User Interaction - Automated services, cron jobs, batch processes
- ✅ Service-to-Service - Microservices communicating with each other
- ✅ AI Agents - Autonomous agents that need to authenticate independently
- ✅ Backend Services - API endpoints that don't have a user session
- ✅ CLI Tools - Command-line tools and scripts
When NOT to Use Client Credentials Flow
Do NOT use this flow when:
- ❌ User-Facing Applications - SPAs, mobile apps, web apps with users
- ❌ Interactive Operations - Any operation requiring user consent or input
- ❌ User-Specific Actions - Actions that should be tied to a specific user identity
How It Works
┌─────────────────────────────────────┐
│ Your Application │
│ (Service, Daemon, Agent) │
│ │
│ Uses Client ID & Secret │
│ │
└────────────┬──────────────────────┘
│
│ POST /oauth/token
│ grant_type=client_credentials
│ client_id=YOUR_CLIENT_ID
│ client_secret=YOUR_CLIENT_SECRET
▼
┌─────────────────────────────────────┐
│ Guardhouse Authorization │
│ │
│ Validates Credentials │
│ │
│ Returns Access Token │
│ │
└────────────┬──────────────────────┘
│
│
▼
┌─────────────────────────────────────┐
│ Protected API │
│ │
│ Receives Access Token │
│ │
│ Authenticates Requests │
│ │
└─────────────────────────────────────┘
Authentication Flow
Step 1: Request Access Token
Endpoint: POST /oauth/token
Content-Type: application/x-www-form-urlencoded
Request Body:
grant_type=client_credentials
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
&audience=https://your_tenant.guardhouse.cloud/api/v2
Example Request:
curl -X POST https://your_tenant.guardhouse.cloud/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" \
-d "audience=https://your_tenant.guardhouse.cloud/api/v2"
Step 2: Receive Access Token
Success Response:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read:users write:users delete:users"
}
Error Response (Invalid Credentials):
{
"error": "invalid_client",
"error_description": "Client authentication failed"
}
Using the Access Token
Once you have an access token, include it in the Authorization header for all API requests:
curl -X GET https://your_tenant.guardhouse.cloud/api/v2/users \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
const response = await fetch('https://your_tenant.guardhouse.cloud/api/v2/users', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
import requests
headers = {
'Authorization': f'Bearer {accessToken}'
}
response = requests.get(
'https://your_tenant.guardhouse.cloud/api/v2/users',
headers=headers
)
Token Management
Token Lifetime
Access tokens obtained via Client Credentials flow have a limited lifetime (default: 1 hour). After expiration, you must request a new token.
Automatic Refresh (Recommended)
For better performance and user experience, implement automatic token refresh:
class TokenManager {
constructor(clientId, clientSecret) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.token = null;
this.expiresAt = null;
}
async getToken() {
// Check if current token is valid and not expired
if (this.token && this.expiresAt > Date.now()) {
return this.token;
}
// Request new token
const response = await fetch('https://your_tenant.guardhouse.cloud/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: this.clientId,
client_secret: this.clientSecret,
audience: 'https://your_tenant.guardhouse.cloud/api/v2',
}),
});
const data = await response.json();
this.token = data.access_token;
this.expiresAt = Date.now() + (data.expires_in * 1000) - 60000; // Refresh 1 minute before expiration
return this.token;
}
}
const tokenManager = new TokenManager(
process.env.GUARDHOUSE_CLIENT_ID,
process.env.GUARDHOUSE_CLIENT_SECRET
);
// Usage
const accessToken = await tokenManager.getToken();
No Refresh Tokens
Client Credentials flow does NOT return refresh tokens. When the access token expires, you must request a new access token using the same client credentials.
Scopes
Define the scopes your application needs when creating the client in Guardhouse Admin Console:
| Scope | Description | Example Use Case |
|---|---|---|
read:users | Read user information | Background user sync, data export |
write:users | Create/update user information | User migration, batch operations |
delete:users | Delete user accounts | Account cleanup, compliance |
read:config | Read application configuration | Monitoring, config management |
write:config | Update application settings | Configuration updates |
Requesting Scopes:
curl -X POST https://your_tenant.guardhouse.cloud/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" \
-d "scope=read:users write:users delete:users"
Security Best Practices
1. Protect Client Secrets
❌ DO NOT:
- Hardcode secrets in your codebase
- Commit secrets to version control (Git, GitHub)
- Log secrets to console or files
- Share secrets in chat, email, or messaging apps
✅ DO:
- Store in environment variables
- Use secret management tools (HashiCorp Vault, AWS Secrets Manager)
- Use CI/CD secrets (GitHub Actions Secrets, GitLab Variables)
- Rotate secrets regularly (every 90 days)
- Use different secrets for development, staging, production
2. Principle of Least Privilege
- Create separate clients with minimal required scopes
- Don't use a single client for all services
- Use different secrets for different services/environments
3. Secure Credential Storage
Environment Variables:
# .env file
GUARDHOUSE_CLIENT_ID=your_client_id
GUARDHOUSE_CLIENT_SECRET=your_client_secret
GUARDHOUSE_DOMAIN=your_tenant.guardhouse.cloud
Docker Secrets:
# docker-compose.yml
version: '3.8'
services:
your-service:
environment:
- GUARDHOUSE_CLIENT_ID=${GUARDHOUSE_CLIENT_ID}
- GUARDHOUSE_CLIENT_SECRET=${GUARDHOUSE_CLIENT_SECRET}
- GUARDHOUSE_DOMAIN=${GUARDHOUSE_DOMAIN}
Kubernetes Secrets:
kubectl create secret generic guardhouse-credentials \
--from-literal=client-id=YOUR_CLIENT_ID \
--from-literal=client-secret=YOUR_CLIENT_SECRET
# Use in deployment
kubectl set env deployment/your-deployment --from=secret/guardhouse-credentials
4. HTTPS Only
Always use HTTPS for all authentication requests. Guardhouse redirects HTTP to HTTPS, but prefer explicit HTTPS URLs.
5. Token Caching
Cache access tokens for performance, but respect expiration:
class TokenCache {
constructor() {
this.cache = new Map();
}
async getToken(clientId, clientSecret) {
const cacheKey = `token_${clientId}`;
// Check cache
if (this.cache.has(cacheKey)) {
const cached = this.cache.get(cacheKey);
if (cached.expiresAt > Date.now()) {
return cached.token;
}
}
// Request new token
const token = await this.requestNewToken(clientId, clientSecret);
// Cache with expiration
this.cache.set(cacheKey, {
token,
expiresAt: Date.now() + 3600000 // 1 hour
});
return token;
}
invalidate(clientId) {
this.cache.delete(`token_${clientId}`);
}
}
Error Handling
Common Errors
| Error | HTTP Status | Cause | Solution |
|---|---|---|---|
invalid_client | 401 | Client ID or secret is incorrect | |
invalid_client_credentials | 401 | Client secret has expired or been rotated | |
unauthorized_client | 403 | Client is disabled or blocked | |
access_denied | 403 | Requested scope is not authorized | |
invalid_scope | 400 | Requested scope is invalid | |
server_error | 500 | Guardhouse server error, retry |
Retry Logic
Implement exponential backoff for transient errors:
class AuthService {
async getTokenWithRetry(maxRetries = 3) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const token = await this.requestToken();
return token;
} catch (error) {
lastError = error;
// Don't retry on authentication errors
if (error.statusCode === 401 || error.statusCode === 403) {
throw error;
}
// Wait before retry (exponential backoff)
if (attempt < maxRetries) {
const waitTime = Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, waitTime));
}
}
}
throw lastError;
}
}
Complete Example
.NET Example
using System;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
public class GuardhouseM2MService
{
private readonly HttpClient _httpClient;
private readonly string _clientId;
private readonly string _clientSecret;
private readonly string _authority;
private string _accessToken;
private DateTime _tokenExpiresAt;
public GuardhouseM2MService(string clientId, string clientSecret, string authority)
{
_httpClient = new HttpClient();
_clientId = clientId;
_clientSecret = clientSecret;
_authority = authority;
}
public async Task<string> GetAccessTokenAsync()
{
// Check if token is still valid
if (_accessToken && DateTime.UtcNow < _tokenExpiresAt)
{
return _accessToken;
}
// Request new token
var parameters = new Dictionary<string, string>
{
{ "grant_type", "client_credentials" },
{ "client_id", _clientId },
{ "client_secret", _clientSecret },
{ "audience", $"{_authority}/api/v2" }
};
var response = await _httpClient.PostAsync(
$"{_authority}/oauth/token",
new FormUrlEncodedContent(parameters)
);
var data = await response.Content.ReadFromJsonAsync<JsonElement>();
_accessToken = data.GetProperty("access_token").GetString();
_tokenExpiresAt = DateTime.UtcNow.AddSeconds(
data.GetProperty("expires_in").GetInt32() - 60 // Buffer
);
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", _accessToken);
return _accessToken;
}
public async Task<T> CallApiAsync<T>(string endpoint)
{
var token = await GetAccessTokenAsync();
var response = await _httpClient.GetAsync($"{_authority}/api/v2{endpoint}");
return await response.Content.ReadFromJsonAsync<T>();
}
}
// Usage
var service = new GuardhouseM2MService(
clientId: "YOUR_CLIENT_ID",
clientSecret: "YOUR_CLIENT_SECRET",
authority: "https://your_tenant.guardhouse.cloud"
);
var users = await service.CallApiAsync<List<User>>("/users");
Node.js Example
const https = require('https');
class GuardhouseM2MService {
constructor(clientId, clientSecret, domain) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.domain = domain;
this.accessToken = null;
this.expiresAt = null;
}
async getAccessToken() {
// Check if token is still valid
if (this.accessToken && Date.now() < this.expiresAt) {
return this.accessToken;
}
// Request new token
const params = new URLSearchParams();
params.append('grant_type', 'client_credentials');
params.append('client_id', this.clientId);
params.append('client_secret', this.clientSecret);
params.append('audience', `${this.domain}/api/v2`);
const response = await https.request(
`${this.domain}/oauth/token`,
{
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: params.toString(),
}
}
);
const data = await JSON.parse(response);
this.accessToken = data.access_token;
this.expiresAt = Date.now() + (data.expires_in * 1000) - 60000;
return this.accessToken;
}
async callApi(endpoint) {
const token = await this.getAccessToken();
const response = await https.request(
`${this.domain}/api/v2${endpoint}`,
{
headers: {
'Authorization': `Bearer ${token}`,
},
}
}
);
return JSON.parse(response);
}
}
// Usage
const service = new GuardhouseM2MService(
process.env.GUARDHOUSE_CLIENT_ID,
process.env.GUARDHOUSE_CLIENT_SECRET,
'https://your_tenant.guardhouse.cloud'
);
const users = await service.callApi('/users');
Python Example
import requests
import time
from datetime import datetime, timedelta
class GuardhouseM2MService:
def __init__(self, client_id, client_secret, domain):
self.client_id = client_id
self.client_secret = client_secret
self.domain = domain
self.access_token = None
self.expires_at = None
def get_access_token(self):
# Check if token is still valid
if self.access_token and datetime.utcnow() < self.expires_at:
return self.access_token
# Request new token
data = {
'grant_type': 'client_credentials',
'client_id': self.client_id,
'client_secret': self.client_secret,
'audience': f'{self.domain}/api/v2'
}
response = requests.post(
f'{self.domain}/oauth/token',
data=data
)
result = response.json()
self.access_token = result['access_token']
self.expires_at = datetime.utcnow() + timedelta(
seconds=result['expires_in'] - 60 # Buffer
)
return self.access_token
def call_api(self, endpoint):
token = self.get_access_token()
headers = {
'Authorization': f'Bearer {token}'
}
response = requests.get(
f'{self.domain}/api/v2{endpoint}',
headers=headers
)
return response.json()
# Usage
service = GuardhouseM2MService(
client_id=os.getenv('GUARDHOUSE_CLIENT_ID'),
client_secret=os.getenv('GUARDHOUSE_CLIENT_SECRET'),
domain='https://your_tenant.guardhouse.cloud'
)
users = service.call_api('/users')
Testing
Using curl
# 1. Get access token
TOKEN_RESPONSE=$(curl -X POST https://your_tenant.guardhouse.cloud/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=$CLIENT_ID" \
-d "client_secret=$CLIENT_SECRET" \
-d "audience=https://your_tenant.guardhouse.cloud/api/v2")
ACCESS_TOKEN=$(echo $TOKEN_RESPONSE | jq -r '.access_token')
# 2. Use token to call API
curl -X GET https://your_tenant.guardhouse.cloud/api/v2/users \
-H "Authorization: Bearer $ACCESS_TOKEN"
Using Postman
- Create a new request
- Set method to
POST - Set URL to
https://your_tenant.guardhouse.cloud/oauth/token - Set headers:
Content-Type:application/x-www-form-urlencoded
- Set body (x-www-form-urlencoded):
grant_type: client_credentials
client_id: YOUR_CLIENT_ID
client_secret: YOUR_CLIENT_SECRET
audience: https://your_tenant.guardhouse.cloud/api/v2 - Send request and copy
access_tokenfrom response - Use the token in subsequent requests with header:
Authorization: Bearer YOUR_ACCESS_TOKEN
Troubleshooting
Issue: "401 Unauthorized"
- Verify client ID is correct
- Check client secret hasn't been rotated
- Ensure client is enabled in Guardhouse Admin Console
- Verify you're using the correct tenant domain
Issue: "403 Forbidden"
- Check if client is blocked or disabled
- Verify requested scopes are granted to the client
- Check rate limits
Issue: "Invalid scope"
- Ensure scopes are granted to the client in Guardhouse Admin Console
- Check scope format (use
read:usersnotread users) - Verify audience parameter matches your API
Related Documentation
- Rate Limiting for Machines - Learn about rate limits
- Introspection for AI Requests - Token validation methods
- User API - User management endpoints
- Authentication API - OAuth 2.0 reference