OAuth2 Client Credentials Flow: Machine-to-Machine Authentication

Type: Software Reference Confidence: 0.92 Sources: 7 Verified: 2026-02-24 Freshness: 2026-02-24

TL;DR

Constraints

Quick Reference

Flow Steps

StepActionDetails
1Register clientCreate M2M application in your IdP; obtain client_id and client_secret
2Configure scopesDefine API permissions — server-side, no user consent needed
3Request tokenPOST /oauth/token with grant_type=client_credentials + credentials + scope
4Auth methodHTTP Basic Authorization: Basic base64(id:secret) or form params
5Receive token{ "access_token": "...", "token_type": "Bearer", "expires_in": 3600 }
6Cache tokenStore in memory until expires_in - 60s buffer
7Use tokenAuthorization: Bearer {access_token}
8Handle expiryRe-request on 401; fetch new token, retry original request
9Rotate credentialsUse two active secrets for zero-downtime rotation
10MonitorTrack issuance rate, failures, scope usage

Token Request Parameters

ParameterRequiredValue
grant_typeYesclient_credentials
client_idYesYour application's client ID
client_secretYesYour application's client secret
scopeRecommendedSpace-separated list of requested scopes
audienceProvider-specificAPI identifier (Auth0, Okta)
resourceProvider-specificResource URI (Microsoft Entra ID)

Common Provider Token Endpoints

ProviderToken Endpoint
Auth0https://{tenant}.auth0.com/oauth/token
Oktahttps://{domain}/oauth2/default/v1/token
Microsoft Entra IDhttps://login.microsoftonline.com/{tenant}/oauth2/v2.0/token
AWS Cognitohttps://{domain}.auth.{region}.amazoncognito.com/oauth2/token
Google Cloudhttps://oauth2.googleapis.com/token
Keycloakhttps://{host}/realms/{realm}/protocol/openid-connect/token

Decision Tree

START: Does your service need to call a protected API?
├── Is there a user context (acting on behalf of a user)?
│   ├── YES → Use Authorization Code flow with PKCE (not Client Credentials)
│   └── NO ↓
├── Is the client a server-side application that can securely store secrets?
│   ├── NO → Cannot use Client Credentials. Use API keys or mTLS instead.
│   └── YES ↓
├── Does the provider support certificate-based auth?
│   ├── YES and security policy requires it → Use client_assertion (JWT signed with cert)
│   └── NO or shared secret is acceptable ↓
├── Does the provider require an audience/resource parameter?
│   ├── Auth0/Okta → Include `audience` parameter
│   ├── Microsoft Entra ID → Include `scope` with `.default` suffix
│   └── Standard → Use `scope` parameter only
└── IMPLEMENT → POST to token endpoint, cache token, add Bearer header to API calls

Step-by-Step Guide

1. Register a Machine-to-Machine application

Create an M2M (non-interactive) application in your identity provider. Record the client_id and client_secret. Store the secret in your secrets manager immediately. [src2]

# Example: Auth0 — create via Management API
curl -X POST "https://{tenant}.auth0.com/api/v2/clients" \
  -H "Authorization: Bearer {mgmt_token}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "My Backend Service",
    "app_type": "non_interactive",
    "grant_types": ["client_credentials"]
  }'

Verify: Check your IdP dashboard — the new application should appear with client_credentials as the only allowed grant type.

2. Configure API permissions (scopes)

Authorize your application to access specific APIs with specific scopes. This is provider-specific but typically done in the IdP admin console. [src2]

# Example: Auth0 — grant API access to the client
curl -X POST "https://{tenant}.auth0.com/api/v2/client-grants" \
  -H "Authorization: Bearer {mgmt_token}" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "{your_client_id}",
    "audience": "https://api.example.com",
    "scope": ["read:data", "write:data"]
  }'

Verify: In IdP console, confirm the application has the expected API grants and scopes listed.

3. Request an access token

Send a POST request to the token endpoint with grant_type=client_credentials. Credentials can be sent as HTTP Basic auth or as POST body parameters. [src1]

# Method A: Credentials in POST body (most common)
curl -X POST "https://{tenant}.auth0.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"

# Method B: HTTP Basic Authentication (RFC 6749 recommended)
curl -X POST "https://{tenant}.auth0.com/oauth/token" \
  -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "audience=https://api.example.com"

Verify: Response is 200 OK with JSON containing access_token, token_type, and expires_in.

4. Cache the token and use it

Cache the token in memory with an expiry buffer. Use it in the Authorization header for all API calls. On 401 Unauthorized, refresh the token and retry. [src5]

import time, threading

class TokenCache:
    """Thread-safe in-memory token cache with expiry buffer."""
    def __init__(self, fetch_fn, buffer_seconds=60):
        self._fetch = fetch_fn
        self._buffer = buffer_seconds
        self._token = None
        self._expires_at = 0
        self._lock = threading.Lock()

    def get_token(self):
        if time.time() < self._expires_at:
            return self._token
        with self._lock:
            if time.time() < self._expires_at:
                return self._token
            data = self._fetch()
            self._token = data["access_token"]
            self._expires_at = time.time() + data["expires_in"] - self._buffer
            return self._token

Verify: Call get_token() twice in succession — the second call should return instantly without an HTTP request.

5. Implement credential rotation

Set up two active client secrets with overlapping validity. Rotate the older secret periodically without downtime. [src6]

# 1. Generate a new secret (provider-specific)
# 2. Deploy the new secret to your service
# 3. Verify the new secret works (test token request)
# 4. Revoke the old secret after deployment is confirmed

Verify: Confirm token requests succeed with the new secret. Monitor for failures during rotation window.

Code Examples

Python: Client Credentials with httpx

# Input:  client_id, client_secret, token_url, audience
# Output: Bearer access token for API calls

import httpx  # httpx >= 0.27.0

def get_client_credentials_token(
    token_url: str,
    client_id: str,
    client_secret: str,
    scope: str = "",
    audience: str = "",
) -> dict:
    """Request token via OAuth2 Client Credentials grant."""
    payload = {
        "grant_type": "client_credentials",
        "client_id": client_id,
        "client_secret": client_secret,
    }
    if scope:
        payload["scope"] = scope
    if audience:
        payload["audience"] = audience

    resp = httpx.post(token_url, data=payload)
    resp.raise_for_status()
    return resp.json()

Node.js: Client Credentials with fetch

// Input:  tokenUrl, clientId, clientSecret, scope
// Output: { access_token, token_type, expires_in }

async function getClientCredentialsToken(tokenUrl, clientId, clientSecret, scope = '') {
  const params = new URLSearchParams({
    grant_type: 'client_credentials',
    client_id: clientId,
    client_secret: clientSecret,
  });
  if (scope) params.set('scope', scope);

  const res = await fetch(tokenUrl, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: params.toString(),
  });
  if (!res.ok) throw new Error(`Token request failed: ${res.status} ${await res.text()}`);
  return res.json();
}

Go: Client Credentials with golang.org/x/oauth2

// Input:  clientID, clientSecret, tokenURL, scopes
// Output: *oauth2.Token with AccessToken field

package main

import (
    "context"
    "fmt"
    "golang.org/x/oauth2"
    "golang.org/x/oauth2/clientcredentials"
)

func getToken(clientID, clientSecret, tokenURL string, scopes []string) (*oauth2.Token, error) {
    cfg := &clientcredentials.Config{
        ClientID:     clientID,
        ClientSecret: clientSecret,
        TokenURL:     tokenURL,
        Scopes:       scopes,
    }
    token, err := cfg.Token(context.Background())
    if err != nil {
        return nil, fmt.Errorf("token request failed: %w", err)
    }
    return token, nil
}

cURL: Direct token request

# Input:  TOKEN_URL, CLIENT_ID, CLIENT_SECRET, SCOPE
# Output: JSON with access_token, token_type, expires_in

curl -s -X POST "$TOKEN_URL" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=$CLIENT_ID" \
  -d "client_secret=$CLIENT_SECRET" \
  -d "scope=$SCOPE" | jq .

Anti-Patterns

Wrong: Hardcoding credentials in source code

# BAD — credentials in source code get committed to git
client_id = "abc123"
client_secret = "super_secret_key_2026"
token = get_token(client_id, client_secret)

Correct: Load credentials from environment or secrets manager

# GOOD — credentials loaded from environment variables
import os
client_id = os.environ["OAUTH_CLIENT_ID"]
client_secret = os.environ["OAUTH_CLIENT_SECRET"]
token = get_token(client_id, client_secret)

Wrong: Requesting a new token on every API call

// BAD — token request on every call adds ~200ms latency + risks rate limiting
async function callApi(endpoint) {
  const { access_token } = await getToken();  // Network call every time!
  return fetch(endpoint, { headers: { Authorization: `Bearer ${access_token}` } });
}

Correct: Cache tokens and reuse until near-expiry

// GOOD — cache token, refresh only when expired (with 60s buffer)
let cached = { token: null, expiresAt: 0 };

async function callApi(endpoint) {
  if (Date.now() / 1000 >= cached.expiresAt) {
    const data = await getToken();
    cached = { token: data.access_token, expiresAt: Date.now() / 1000 + data.expires_in - 60 };
  }
  return fetch(endpoint, { headers: { Authorization: `Bearer ${cached.token}` } });
}

Wrong: Requesting all available scopes

# BAD — over-scoped token: if compromised, attacker has full access
curl -X POST "$TOKEN_URL" -d "grant_type=client_credentials" \
  -d "client_id=$ID" -d "client_secret=$SECRET" \
  -d "scope=read:all write:all admin:all delete:all"

Correct: Request minimum required scopes

# GOOD — only request what this specific service needs
curl -X POST "$TOKEN_URL" -d "grant_type=client_credentials" \
  -d "client_id=$ID" -d "client_secret=$SECRET" \
  -d "scope=read:orders write:shipments"

Wrong: Using Client Credentials from a browser

// BAD — client_secret exposed in browser JS, visible in DevTools
const token = await fetch('/oauth/token', {
  method: 'POST',
  body: new URLSearchParams({
    grant_type: 'client_credentials',
    client_id: 'my_client_id',
    client_secret: 'EXPOSED_SECRET',  // Anyone can see this!
  }),
});

Correct: Use Authorization Code with PKCE for browser apps

// GOOD — PKCE flow for browser: no client_secret needed
window.location.href = `${authUrl}/authorize?` +
  `response_type=code&client_id=${clientId}&` +
  `redirect_uri=${redirectUri}&` +
  `code_challenge=${codeChallenge}&code_challenge_method=S256`;

Common Pitfalls

Diagnostic Commands

# Test token request (replace variables)
curl -v -X POST "$TOKEN_URL" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET&scope=$SCOPE"

# Decode a JWT access token (inspect claims without verification)
echo "$ACCESS_TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq .

# Check token expiry from JWT claims
echo "$ACCESS_TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq '{exp: .exp, exp_human: (.exp | todate), iss: .iss, aud: .aud, scope: .scope}'

# Verify token against an API endpoint
curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $ACCESS_TOKEN" "$API_URL/health"

# Check provider token endpoint connectivity
curl -s -o /dev/null -w "HTTP %{http_code} in %{time_total}s\n" -X POST "$TOKEN_URL" -d ""

# Monitor token request latency
time curl -s -X POST "$TOKEN_URL" \
  -d "grant_type=client_credentials&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET" > /dev/null

Version History & Compatibility

Spec/VersionStatusKey ChangesNotes
RFC 6749 (2012)Current StandardDefined Client Credentials grant in Section 4.4Foundation spec; all providers implement this
OAuth 2.1 (draft)Draft (expected 2025-2026)Preserves client_credentials unchangedRemoves implicit + ROPC grants
RFC 7523 (2015)CurrentJWT Profile for client authenticationAlternative to shared secrets; uses signed JWT
RFC 8705 (2020)CurrentMutual TLS sender-constrained tokensStrongest auth method for client credentials
BCP (draft-29, 2024)Draft BCPRecommends asymmetric client authGuidance, not a breaking change

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
Service-to-service / M2M communicationUser needs to grant consentAuthorization Code + PKCE
Backend cron jobs calling APIsClient is a browser SPAAuthorization Code + PKCE
Microservice-to-microservice authClient is a mobile/desktop appAuthorization Code + PKCE
CI/CD pipeline API accessIoT device with no secure storageDevice Authorization Grant
Daemon processes and workersSimple internal API with no OAuth infraAPI keys or mTLS
Automated data sync between servicesNeed user-specific data/permissionsAuthorization Code + PKCE

Important Caveats

Related Units