OAuth 2.0 Client Credentials: Complete Guide (2026)
Implement machine-to-machine authentication the right way
Start Building with Hypereal
Access Kling, Flux, Sora, Veo & more through a single API. Free credits to start, scale to millions.
No credit card required • 100k+ developers • Enterprise ready
OAuth 2.0 Client Credentials: Complete Guide (2026)
The OAuth 2.0 Client Credentials grant is the standard way to authenticate machine-to-machine (M2M) communication. Unlike the Authorization Code flow (which involves a user logging in through a browser), the Client Credentials flow is designed for server-to-server scenarios where no human user is involved -- background jobs, microservices, CLI tools, and automated pipelines.
This guide covers how the Client Credentials flow works, when to use it, how to implement it in multiple languages, and the security best practices you should follow.
When to Use Client Credentials
| Scenario | Use Client Credentials? | Why |
|---|---|---|
| Backend service calling another API | Yes | No user context needed |
| Cron job fetching data from an API | Yes | Automated, no user interaction |
| Microservice-to-microservice calls | Yes | Server-to-server authentication |
| Mobile app calling your API | No | Use Authorization Code + PKCE |
| Web app on behalf of a user | No | Use Authorization Code flow |
| Single-page app (SPA) | No | Use Authorization Code + PKCE |
| IoT device reporting data | Maybe | Depends on device capabilities |
The rule is simple: if there is no human user involved in the request, Client Credentials is likely the right flow.
How the Flow Works
The Client Credentials flow is the simplest OAuth 2.0 grant. It involves two parties and one request:
┌──────────┐ ┌────────────────────┐
│ Client │ │ Authorization │
│ (Your │──── 1. Request Token ───>│ Server │
│ Server) │ (client_id + │ │
│ │ client_secret) │ │
│ │<─── 2. Access Token ─────│ │
│ │ └────────────────────┘
│ │
│ │ ┌────────────────────┐
│ │──── 3. API Request ─────>│ Resource Server │
│ │ (with access token) │ (The API) │
│ │<─── 4. Response ─────────│ │
└──────────┘ └────────────────────┘
Step 1: Your client (server, script, or service) sends its client_id and client_secret to the authorization server's token endpoint.
Step 2: The authorization server validates the credentials and returns an access token.
Step 3: Your client uses the access token to call the resource server (the API you want to access).
Step 4: The resource server validates the token and returns the response.
Implementation
Step 1: Register Your Application
Before you can use the Client Credentials flow, you need to register your application with the authorization server (for example, Auth0, Okta, Azure AD, or your own OAuth server).
You will receive:
- Client ID: A public identifier for your application.
- Client Secret: A confidential key that must be kept secure.
Step 2: Request an Access Token
Python
import requests
TOKEN_URL = "https://auth.example.com/oauth/token"
CLIENT_ID = "your-client-id"
CLIENT_SECRET = "your-client-secret"
AUDIENCE = "https://api.example.com" # The API you want to access
response = requests.post(
TOKEN_URL,
data={
"grant_type": "client_credentials",
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"audience": AUDIENCE,
"scope": "read:data write:data",
},
headers={"Content-Type": "application/x-www-form-urlencoded"},
)
token_data = response.json()
access_token = token_data["access_token"]
expires_in = token_data["expires_in"] # Typically 3600 seconds (1 hour)
print(f"Token: {access_token[:20]}...")
print(f"Expires in: {expires_in} seconds")
Node.js
const TOKEN_URL = "https://auth.example.com/oauth/token";
async function getAccessToken() {
const response = await fetch(TOKEN_URL, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "client_credentials",
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET,
audience: "https://api.example.com",
scope: "read:data write:data",
}),
});
if (!response.ok) {
throw new Error(`Token request failed: ${response.status}`);
}
const data = await response.json();
return data.access_token;
}
cURL
curl -X POST https://auth.example.com/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://api.example.com" \
-d "scope=read:data write:data"
Alternative: HTTP Basic Authentication
Some authorization servers expect the client credentials to be sent as an HTTP Basic auth header instead of in the request body:
import requests
from requests.auth import HTTPBasicAuth
response = requests.post(
TOKEN_URL,
data={
"grant_type": "client_credentials",
"scope": "read:data write:data",
},
auth=HTTPBasicAuth(CLIENT_ID, CLIENT_SECRET),
)
The equivalent in cURL:
curl -X POST https://auth.example.com/oauth/token \
-u "your-client-id:your-client-secret" \
-d "grant_type=client_credentials" \
-d "scope=read:data write:data"
Step 3: Use the Access Token
Once you have the token, include it in the Authorization header of your API requests:
api_response = requests.get(
"https://api.example.com/v1/data",
headers={
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
},
)
print(api_response.json())
Token Response Format
A successful token response looks like this:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read:data write:data"
}
| Field | Description |
|---|---|
access_token |
The JWT or opaque token to use in API requests |
token_type |
Always "Bearer" for this flow |
expires_in |
Token lifetime in seconds |
scope |
The granted scopes (may differ from requested scopes) |
Note: The Client Credentials flow does not return a refresh_token. When the access token expires, you simply request a new one using the same client credentials.
Token Caching and Management
Requesting a new token for every API call is wasteful and can hit rate limits on the authorization server. Implement token caching:
import time
import requests
class TokenManager:
def __init__(self, token_url, client_id, client_secret, audience, scope=""):
self.token_url = token_url
self.client_id = client_id
self.client_secret = client_secret
self.audience = audience
self.scope = scope
self._token = None
self._expires_at = 0
def get_token(self):
# Return cached token if still valid (with 60s buffer)
if self._token and time.time() < self._expires_at - 60:
return self._token
response = requests.post(
self.token_url,
data={
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret,
"audience": self.audience,
"scope": self.scope,
},
)
response.raise_for_status()
data = response.json()
self._token = data["access_token"]
self._expires_at = time.time() + data["expires_in"]
return self._token
# Usage
token_manager = TokenManager(
token_url="https://auth.example.com/oauth/token",
client_id="your-client-id",
client_secret="your-client-secret",
audience="https://api.example.com",
)
# Always use token_manager.get_token() - it handles caching automatically
headers = {"Authorization": f"Bearer {token_manager.get_token()}"}
Provider-Specific Examples
Auth0
response = requests.post(
"https://your-tenant.auth0.com/oauth/token",
json={
"grant_type": "client_credentials",
"client_id": "your-client-id",
"client_secret": "your-client-secret",
"audience": "https://your-api-identifier",
},
headers={"Content-Type": "application/json"},
)
Note: Auth0 uses application/json instead of form-encoded data.
Azure AD (Microsoft Entra ID)
tenant_id = "your-tenant-id"
response = requests.post(
f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token",
data={
"grant_type": "client_credentials",
"client_id": "your-client-id",
"client_secret": "your-client-secret",
"scope": "https://graph.microsoft.com/.default",
},
)
Okta
response = requests.post(
"https://your-org.okta.com/oauth2/default/v1/token",
data={
"grant_type": "client_credentials",
"scope": "custom_scope",
},
auth=("your-client-id", "your-client-secret"),
)
Security Best Practices
1. Never Expose Client Secrets
- Do not commit secrets to source control. Use environment variables or a secrets manager.
- Do not include secrets in client-side code (JavaScript bundles, mobile apps).
- Rotate secrets periodically (every 90 days is a common policy).
# Good: environment variable
export CLIENT_SECRET="your-secret-here"
# Good: secrets manager
aws secretsmanager get-secret-value --secret-id oauth/client-secret
2. Use the Principle of Least Privilege
Request only the scopes your application needs. Do not request admin:* if you only need read:data.
3. Validate Tokens on the Resource Server
If you are building the API that receives the token, always validate:
import jwt
from jwt import PyJWKClient
JWKS_URL = "https://auth.example.com/.well-known/jwks.json"
AUDIENCE = "https://api.example.com"
ISSUER = "https://auth.example.com/"
jwks_client = PyJWKClient(JWKS_URL)
def validate_token(token: str) -> dict:
signing_key = jwks_client.get_signing_key_from_jwt(token)
payload = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
audience=AUDIENCE,
issuer=ISSUER,
)
return payload
4. Use Short-Lived Tokens
Configure your authorization server to issue tokens with a short lifetime (1 hour or less). Since the Client Credentials flow does not use refresh tokens, your client simply requests a new token when the current one expires.
5. Monitor and Audit
Log all token requests and API calls. Set up alerts for unusual patterns like:
- Sudden spikes in token requests
- Requests from unexpected IP addresses
- Failed authentication attempts
Common Errors and Solutions
| Error | Cause | Fix |
|---|---|---|
invalid_client |
Wrong client_id or client_secret | Verify credentials, check for copy-paste issues |
invalid_scope |
Requested scope not configured | Check which scopes are allowed for your client |
unauthorized_client |
Client not authorized for this grant type | Enable "Client Credentials" grant in your auth server config |
invalid_grant |
General grant error | Check audience, token URL, and credentials |
access_denied |
Client lacks permission | Verify API permissions assigned to the client |
Conclusion
The OAuth 2.0 Client Credentials flow is straightforward to implement and is the standard approach for machine-to-machine authentication. The key points to remember are: cache your tokens, keep secrets secure, request minimal scopes, and validate tokens properly on the server side.
If you are building applications that integrate AI media generation APIs -- for images, videos, talking avatars, or audio -- alongside your authenticated services, check out Hypereal AI. Hypereal provides a unified API with simple API key authentication and pay-as-you-go pricing for the latest generative AI models.
Related Articles
Start Building Today
Get 35 free credits on signup. No credit card required. Generate your first image in under 5 minutes.
