OAuth 2.0 Client Credentials — Complete Server-to-Server Auth Guide
In this tutorial, you will learn about OAuth 2.0 Client Credentials. We cover key concepts, practical examples, and best practices to help you master this topic.
The client credentials grant is an OAuth 2.0 flow for machine-to-machine communication. The client authenticates using its own credentials (client_id and client_secret) and receives an access token without any user involvement.
What You'll Learn
You'll learn how client credentials work, when to use them, and how to implement secure service-to-service authentication.
Why It Matters
Microservices need to authenticate to each other. Client credentials provide a standardized, secure way for services to obtain tokens without user context.
Real-World Use
A background job service uses client credentials to authenticate to the main API. Every 15 minutes, it obtains a new token to Process pending tasks, with no user interaction required.
sequenceDiagram
participant ServiceA as Backend Service
participant Auth as Authorization Server
participant API as Protected API
ServiceA->>Auth: POST /token (client_id, client_secret, scope)
Auth->>ServiceA: access_token
ServiceA->>API: GET /data (Bearer token)
API->>Auth: Validate token (optional introspection)
Auth-->>API: Token valid
API->>ServiceA: Data
Teacher's Mindset
Client credentials are like a company badge that gives you access to the server room. The badge identifies you as an employee (not a specific person) and grants machine-level permissions.
Implementation
from flask import Flask, request, jsonify
import jwt
from datetime import datetime, timedelta
import secrets
app = Flask(__name__)
CLIENTS = {
"batch-processor": {"secret": "hashed-secret-1", "scopes": ["jobs:read", "jobs:write"]},
"report-generator": {"secret": "hashed-secret-2", "scopes": ["reports:read"]}
}
@app.route("/oauth/token", methods=["POST"])
def token():
client_id = request.form.get("client_id")
client_secret = request.form.get("client_secret")
scope = request.form.get("scope", "")
grant_type = request.form.get("grant_type")
if grant_type != "client_credentials":
return jsonify({"error": "unsupported_grant_type"}), 400
client = CLIENTS.get(client_id)
if not client or client["secret"] != client_secret:
return jsonify({"error": "invalid_client"}), 401
token = jwt.encode({
"sub": client_id,
"scope": scope,
"iat": datetime.utcnow(),
"exp": datetime.utcnow() + timedelta(hours=1)
}, "secret-key", algorithm="HS256")
return jsonify({
"access_token": token,
"token_type": "Bearer",
"expires_in": 3600,
"scope": scope
})
# Client-side usage
def call_protected_api():
response = requests.post(
"https://auth.example.com/oauth/token",
data={
"grant_type": "client_credentials",
"client_id": "batch-processor",
"client_secret": "your-secret",
"scope": "jobs:read jobs:write"
}
)
access_token = response.json()["access_token"]
headers = {"Authorization": f"Bearer {access_token}"}
result = requests.get("https://api.example.com/jobs", headers=headers)
return result.json()
Common Mistakes
| Mistake | Fix |
|---|---|
| Hardcoding client secrets in code | Use environment variables or vault |
| No scope restriction | Limit tokens to minimum required scopes |
| Long token lifetimes | Keep tokens under 1 hour for services |
| No token revocation | Maintain token blacklist or short expiry |
| Client credentials for user delegation | Use authorization code for user context |
Practice Questions
- When should you use client credentials vs authorization code?
- How do scopes limit access in client credentials?
- Why is client credentials unsuitable for user-facing apps?
- How do you rotate client secrets?
- What is token introspection?
Challenge
Implement a client credentials server and two microservices. Service A uses client credentials to obtain a token and call Service B. Service B validates the token and checks scopes.
What's Next
Learn about OAuth 2.0 PKCE for secure mobile and SPA authentication.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro