Skip to content

OAuth2 Client Credentials Deep Dive — Server-to-Server Authentication

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about OAuth2 Client Credentials Deep Dive. We cover key concepts, practical examples, and best practices to help you master this topic.

OAuth2 Client Credentials grant allows services to authenticate directly without user involvement, issuing tokens scoped to the client application rather than a human user.

What You'll Learn

Client Credentials flow implementation, client authentication methods (client_secret_basic, client_secret_post, private_key_jwt), scoping machine tokens, and token exchange between services.

Why It Matters

Microservice architectures need service-to-service authentication. Client Credentials provides a standardized way for services to obtain tokens without storing long-lived API keys or impersonating users.

Real-World Use

Stripe uses Client Credentials for server-side API access. AWS services use a variant for inter-service auth. Durga Antivirus Pro microservices use Client Credentials to communicate — the scanning service obtains tokens to call the threat intelligence service.

flowchart LR
    A["Microservice A\n(scanner)"] -->|"POST /token\ngrant_type=client_credentials\nclient_id=scanner\nclient_secret=..."| B["Auth Server"]
    B -->|"Signed JWT\nscope=threat:read"| A
    A -->|"GET /threats\nBearer JWT"| C["Microservice B\n(threat-intel)"]
    C -->|"Verify token\n+ check scope"| D["200 OK"]
    style A fill:#dbeafe,stroke:#2563eb
    style B fill:#fef3c7,stroke:#d97706
    style C fill:#fef3c7,stroke:#d97706
    style D fill:#dcfce7,stroke:#16a34a

Code Example: Client Credentials Token Endpoint

import jwt, datetime, secrets, hashlib, base64
from flask import Flask, request, jsonify

app = Flask(__name__)
SECRET = "oauth2-secret-key-change-in-production"

# Registered services
clients = {
    "scanner-service": {
        "secret_hash": hashlib.sha256("scanner-secret-123".encode()).hexdigest(),
        "scopes": ["threat:read", "scan:write"],
        "service_name": "Vulnerability Scanner"
    },
    "notification-service": {
        "secret_hash": hashlib.sha256("notify-secret-456".encode()).hexdigest(),
        "scopes": ["alert:write"],
        "service_name": "Alert Dispatcher"
    }
}

@app.route("/oauth/token", methods=["POST"])
def token():
    client_id = request.form.get("client_id")
    client_secret = request.form.get("client_secret")
    grant_type = request.form.get("grant_type")
    scope = request.form.get("scope", "")
    client = clients.get(client_id)

    if grant_type != "client_credentials":
        return jsonify({"error": "unsupported_grant_type"}), 400

    if not client:
        return jsonify({"error": "invalid_client"}), 401

    # Validate client_secret
    secret_hash = hashlib.sha256(client_secret.encode()).hexdigest()
    if secret_hash != client["secret_hash"]:
        return jsonify({"error": "invalid_client"}), 401

    # Validate requested scopes are within allowed scopes
    requested_scopes = scope.split() if scope else client["scopes"]
    allowed_scopes = client["scopes"]
    for s in requested_scopes:
        if s not in allowed_scopes:
            return jsonify({"error": "invalid_scope"}), 400

    token = jwt.encode({
        "iss": "auth.dodatech.com",
        "sub": client_id,
        "aud": "api.dodatech.com",
        "scope": " ".join(requested_scopes),
        "iat": datetime.datetime.utcnow(),
        "exp": datetime.datetime.utcnow() + datetime.timedelta(hours=1),
        "jti": secrets.token_hex(16),
        "client_id": client_id
    }, SECRET, algorithm="HS256")

    return jsonify({
        "access_token": token,
        "token_type": "Bearer",
        "expires_in": 3600,
        "scope": " ".join(requested_scopes)
    })

Code Example: Client Authentication with Private Key JWT

import jwt as pyjwt
from cryptography.hazmat.primitives import serialization

# Client authenticates using a signed JWT instead of client_secret
def validate_private_key_jwt(client_assertion, client_id):
    """Validate client using private_key_jwt client authentication."""
    try:
        # The client's public key must be registered
        client_public_key = get_client_public_key(client_id)

        header = pyjwt.get_unverified_header(client_assertion)
        payload = pyjwt.decode(
            client_assertion,
            client_public_key,
            algorithms=["RS256", "ES256"]
        )

        # Validate JWT contents
        if payload.get("iss") != client_id:
            return False
        if payload.get("sub") != client_id:
            return False
        if "auth.dodatech.com/token" not in payload.get("aud", []):
            return False
        if datetime.datetime.utcfromtimestamp(payload.get("exp", 0)) < datetime.datetime.utcnow():
            return False

        return True
    except Exception:
        return False

@app.route("/oauth/token", methods=["POST"])
def token_with_jwt_auth():
    client_id = request.form.get("client_id")
    client_assertion = request.form.get("client_assertion")
    client_assertion_type = request.form.get("client_assertion_type")

    expected_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
    if client_assertion_type != expected_type:
        return jsonify({"error": "invalid_client"}), 401

    if not validate_private_key_jwt(client_assertion, client_id):
        return jsonify({"error": "invalid_client"}), 401

    # Issue token...
    return jsonify({"access_token": "...", "token_type": "Bearer"})

Code Example: Service-to-Service Token Exchange

import requests

class ServiceClient:
    """Generic service client using Client Credentials."""

    def __init__(self, auth_url, client_id, client_secret, scopes):
        self.auth_url = auth_url
        self.client_id = client_id
        self.client_secret = client_secret
        self.scopes = scopes
        self.token = None
        self.token_expiry = 0

    def _get_token(self):
        if self.token and time.time() < self.token_expiry - 60:
            return self.token

        resp = requests.post(self.auth_url, data={
            "grant_type": "client_credentials",
            "client_id": self.client_id,
            "client_secret": self.client_secret,
            "scope": " ".join(self.scopes)
        })

        data = resp.json()
        self.token = data["access_token"]
        self.token_expiry = time.time() + data["expires_in"]
        return self.token

    def request(self, method, url, **kwargs):
        token = self._get_token()
        headers = kwargs.pop("headers", {})
        headers["Authorization"] = f"Bearer {token}"
        return requests.request(method, url, headers=headers, **kwargs)

# Usage
scanner_client = ServiceClient(
    auth_url="https://auth.dodatech.com/oauth/token",
    client_id="scanner-service",
    client_secret="scanner-secret-123",
    scopes=["threat:read"]
)

response = scanner_client.request("GET", "https://api.dodatech.com/v1/threats")

Common Mistakes

1. Using Client Credentials for User Actions

Client Credentials represents the application, not a user. Do not use it for actions that require user context or consent. Use Authorization Code grant instead.

2. Storing Client Secrets in Code

Client secrets are credentials. Store them in environment variables, secrets managers, or encrypted config files. Never commit them to version control.

3. Over-Scoping Service Tokens

Grant each service the minimum scope it needs. A notification service should not have write access to threat data. Follow the principle of Least Privilege.

4. No Token Caching

Requesting a new token for every API call creates unnecessary load. Cache the token and refresh it before expiry (with a 60-second buffer).

5. Using Symmetric Secrets for High-Value Services

For critical services, use private_key_jwt with asymmetric keys. The client proves possession of the private key without sharing a secret.

Practice Questions

  1. When should Client Credentials grant be used?
  2. What are the three client authentication methods for Client Credentials?
  3. How does scope validation work in the token endpoint?
  4. Why should service tokens be cached?
  5. What is the advantage of private_key_jwt over client_secret_basic?

Answers:

  1. For machine-to-machine communication where no user is involved. Services, daemons, Cron Jobs, and backend processes.
  2. client_secret_basic (HTTP Basic Auth), client_secret_post (form parameter), private_key_jwt (signed JWT assertion).
  3. The token endpoint validates that requested scopes are a subset of the client's registered allowed scopes. Invalid scopes return invalid_scope error.
  4. Token generation involves signing and potentially database lookups. Caching reduces latency and auth server load. Cache with a buffer before actual expiry.
  5. The client proves identity with a private key instead of a shared secret. If the client is compromised, the private key can be rotated without changing secrets on the server.

Challenge: Build a microservice architecture with three services: auth-server (issues tokens), data-service (protected by scoped tokens), and a client service that obtains tokens using private_key_jwt authentication.

FAQ

Can Client Credentials tokens be refreshed?

Usually not. The token is valid for a fixed period. When it expires, the client requests a new one using its credentials. No refresh token is needed.

How long should Client Credentials tokens live?

30 minutes to 24 hours. Shorter tokens increase security but create more auth server load. Cache them on the client side.

Is Client Credentials suitable for third-party apps?

No. Client Credentials gives the application full access within its scope. For third-party access, use Authorization Code grant with user consent.

How do I revoke a Client Credentials token?

Revoke the client's access by removing the client registration or changing the secret. Existing tokens expire according to their exp claim.

Can I use Client Credentials with OAuth2 scopes from JWT claims?

Yes. Encode granted scopes in the token's scope claim. The resource server checks this claim to authorize specific operations.

Mini Project

Build an OAuth2 authorization server with Client Credentials grant supporting client_secret_basic and private_key_jwt authentication. Include a test service that obtains, caches, and uses tokens to call protected endpoints.

What's Next

Now learn about OAuth2 Resource Owner Password Grant for legacy first-party applications.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro