Skip to main content

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.

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:

ScopeDescriptionExample Use Case
read:usersRead user informationBackground user sync, data export
write:usersCreate/update user informationUser migration, batch operations
delete:usersDelete user accountsAccount cleanup, compliance
read:configRead application configurationMonitoring, config management
write:configUpdate application settingsConfiguration 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

ErrorHTTP StatusCauseSolution
invalid_client401Client ID or secret is incorrect
invalid_client_credentials401Client secret has expired or been rotated
unauthorized_client403Client is disabled or blocked
access_denied403Requested scope is not authorized
invalid_scope400Requested scope is invalid
server_error500Guardhouse 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

  1. Create a new request
  2. Set method to POST
  3. Set URL to https://your_tenant.guardhouse.cloud/oauth/token
  4. Set headers:
    • Content-Type: application/x-www-form-urlencoded
  5. 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
  6. Send request and copy access_token from response
  7. 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:users not read users)
  • Verify audience parameter matches your API