← Back to Blog

Client Credentials Flow for Service-to-Service Authentication

oauthclient-credentialssecurity

Client Credentials Flow for Service-to-Service Authentication

April 15, 2026
2,856 views
4.0
Paal Gyula
Paal Gyula
gyula@pilab.hu

Understanding OAuth 2.1 Client Credentials flow for secure machine-to-machine communication


Client Credentials Flow for Service-to-Service Authentication

The Client Credentials flow is the OAuth 2.1 grant type designed for machine-to-machine authentication where no user context is involved. It's the standard approach for service-to-service communication in microservices architectures.

When to Use Client Credentials Flow

The Client Credentials flow is appropriate when:

  • Services need to authenticate with other services
  • No end-user context is required
  • The client is acting on its own behalf (not on behalf of a user)
  • Backend systems need to access protected APIs
  • Microservices need to establish trust with each other

The Client Credentials Flow

Step 1: Token Request

The client authenticates with the authorization server using its credentials:

http
1POST /token HTTP/1.1
2Host: auth.example.com
3Content-Type: application/x-www-form-urlencoded
4Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
5
6grant_type=client_credentials&
7scope=api:read api:write

Key components:

  • grant_type=client_credentials: Specifies the OAuth grant type
  • Authorization: Basic ...: Base64-encoded client_id:client_secret
  • scope: Optional scopes requested for the access token

Step 2: Token Response

The authorization server validates the client credentials and issues an access token:

json
1{
2  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
3  "token_type": "Bearer",
4  "expires_in": 3600,
5  "scope": "api:read api:write"
6}

Note: No refresh token is issued in the Client Credentials flow since the client can always request a new token using its credentials.

Step 3: Accessing Protected Resources

The client uses the access token to make authenticated requests:

http
1GET /api/v1/users HTTP/1.1
2Host: api.example.com
3Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...

Client Authentication Methods

OAuth 2.1 supports several client authentication methods:

1. Client Secret Basic (HTTP Basic Auth)

Authorization: Base64(client_id:client_secret)

Most common method, but requires secure storage of the client secret.

2. Client Secret Post

http
1POST /token HTTP/1.1
2Content-Type: application/x-www-form-urlencoded
3
4grant_type=client_credentials&
5client_id=s6BhdRkqt3&
6client_secret=gX1fBat3bV&
7scope=api:read

Sends credentials in the request body instead of the Authorization header.

3. Private Key JWT (Recommended for High Security)

http
1POST /token HTTP/1.1
2Content-Type: application/x-www-form-urlencoded
3
4grant_type=client_credentials&
5client_id=s6BhdRkqt3&
6client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&
7client_assertion=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...

Uses asymmetric cryptography: the client signs a JWT with its private key, and the authorization server verifies it with the client's public key.

Implementation Example

Node.js/TypeScript Implementation

typescript
1import crypto from 'crypto';
2
3interface TokenResponse {
4  access_token: string;
5  token_type: string;
6  expires_in: number;
7  scope: string;
8}
9
10class ClientCredentialsAuth {
11  private tokenCache: {
12    token: string;
13    expiresAt: number;
14  } | null = null;
15
16  constructor(
17    private clientId: string,
18    private clientSecret: string,
19    private tokenUrl: string,
20    private scopes: string[] = [],
21  ) {}
22
23  async getToken(): Promise<string> {
24    // Return cached token if still valid
25    if (this.tokenCache && Date.now() < this.tokenCache.expiresAt) {
26      return this.tokenCache.token;
27    }
28
29    const response = await fetch(this.tokenUrl, {
30      method: 'POST',
31      headers: {
32        'Content-Type': 'application/x-www-form-urlencoded',
33        Authorization: `Basic ${Buffer.from(
34          `${this.clientId}:${this.clientSecret}`,
35        ).toString('base64')}`,
36      },
37      body: new URLSearchParams({
38        grant_type: 'client_credentials',
39        scope: this.scopes.join(' '),
40      }),
41    });
42
43    if (!response.ok) {
44      throw new Error(`Token request failed: ${response.statusText}`);
45    }
46
47    const data: TokenResponse = await response.json();
48
49    // Cache token with 5-minute buffer
50    this.tokenCache = {
51      token: data.access_token,
52      expiresAt: Date.now() + (data.expires_in - 300) * 1000,
53    };
54
55    return data.access_token;
56  }
57
58  async fetchWithAuth(
59    url: string,
60    options: RequestInit = {},
61  ): Promise<Response> {
62    const token = await this.getToken();
63
64    return fetch(url, {
65      ...options,
66      headers: {
67        ...options.headers,
68        Authorization: `Bearer ${token}`,
69      },
70    });
71  }
72}

Python Implementation

python
1import time
2import requests
3from typing import Optional
4
5class ClientCredentialsAuth:
6    def __init__(
7        self,
8        client_id: str,
9        client_secret: str,
10        token_url: str,
11        scopes: list[str] | None = None
12    ):
13        self.client_id = client_id
14        self.client_secret = client_secret
15        self.token_url = token_url
16        self.scopes = scopes or []
17        self._token_cache: Optional[dict] = None
18
19    def get_token(self) -> str:
20        if self._token_cache and time.time() < self._token_cache['expires_at']:
21            return self._token_cache['access_token']
22
23        response = requests.post(
24            self.token_url,
25            auth=(self.client_id, self.client_secret),
26            data={
27                'grant_type': 'client_credentials',
28                'scope': ' '.join(self.scopes),
29            }
30        )
31        response.raise_for_status()
32
33        token_data = response.json()
34        # Cache with 5-minute buffer
35        self._token_cache = {
36            **token_data,
37            'expires_at': time.time() + token_data['expires_in'] - 300
38        }
39
40        return token_data['access_token']
41
42    def make_request(self, url: str, **kwargs) -> requests.Response:
43        headers = kwargs.pop('headers', {})
44        headers['Authorization'] = f'Bearer {self.get_token()}'
45
46        return requests.request(url=url, headers=headers, **kwargs)

Security Best Practices

1. Secure Client Secret Storage

bash
1# NEVER store secrets in code or environment files committed to version control
2# Use secure secret management:
3
4# AWS Secrets Manager
5aws secretsmanager get-secret-value --secret-id my-oauth-client
6
7# HashiCorp Vault
8vault kv get secret/oauth/clients/my-service
9
10# Azure Key Vault
11az keyvault secret show --vault-name my-vault --name client-secret

2. Implement Token Caching

Always cache tokens to avoid unnecessary requests to the authorization server. Implement proper expiration handling with a buffer period.

3. Use Minimal Scopes

Request only the scopes your service actually needs:

json
1{
2  "good": ["orders:read", "users:read"],
3  "bad": ["*"]
4}

4. Implement Circuit Breakers

typescript
1class CircuitBreaker {
2  private failures = 0;
3  private lastFailureTime = 0;
4  private readonly threshold = 5;
5  private readonly resetTimeout = 30000; // 30 seconds
6
7  async execute<T>(fn: () => Promise<T>): Promise<T> {
8    if (this.isOpen()) {
9      throw new Error('Circuit breaker is open');
10    }
11
12    try {
13      const result = await fn();
14      this.onSuccess();
15      return result;
16    } catch (error) {
17      this.onFailure();
18      throw error;
19    }
20  }
21
22  private isOpen(): boolean {
23    if (this.failures >= this.threshold) {
24      const timeSinceLastFailure = Date.now() - this.lastFailureTime;
25      if (timeSinceLastFailure < this.resetTimeout) {
26        return true;
27      }
28      this.failures = 0; // Reset after timeout
29    }
30    return false;
31  }
32
33  private onSuccess() {
34    this.failures = 0;
35  }
36
37  private onFailure() {
38    this.failures++;
39    this.lastFailureTime = Date.now();
40  }
41}

5. Monitor and Alert

Track these metrics for your Client Credentials implementation:

  • Token request success/failure rates
  • Token refresh frequency
  • Authorization server response times
  • Unusual token request patterns (potential abuse)

Common Pitfalls

1. Not Handling Token Expiration

typescript
1// BAD: No token caching, requests new token every time
2async function callApi() {
3  const token = await requestNewToken();
4  return fetch('/api/data', {
5    headers: { Authorization: `Bearer ${token}` },
6  });
7}
8
9// GOOD: Cache tokens with expiration
10const tokenManager = new TokenManager();
11async function callApi() {
12  const token = await tokenManager.getValidToken();
13  return fetch('/api/data', {
14    headers: { Authorization: `Bearer ${token}` },
15  });
16}

2. Over-Privileged Tokens

json
1{
2  "bad": {
3    "scope": "admin:*"
4  },
5  "good": {
6    "scope": "orders:read inventory:read"
7  }
8}

3. Hardcoded Credentials

typescript
1// NEVER DO THIS
2const clientId = 'my-client-id';
3const clientSecret = 'my-super-secret-key';
4
5// DO THIS INSTEAD
6const clientId = process.env.OAUTH_CLIENT_ID;
7const clientSecret = process.env.OAUTH_CLIENT_SECRET;

Q&A: Client Credentials Flow

Frequently Asked Questions

When should I use Client Credentials vs other OAuth flows?

Use Client Credentials when:

  • Service-to-service communication without user context
  • Backend systems accessing APIs on their own behalf
  • Microservices authenticating with each other
  • Automated processes accessing protected resources

Use Authorization Code Flow when a user context is required. Use Device Authorization Grant for headless devices.

How do I rotate client secrets without downtime?

Implement a dual-secret strategy:

  1. Generate a new client secret while keeping the old one active
  2. Update your services to use the new secret
  3. Verify all services are using the new secret
  4. Remove the old secret from the authorization server
  5. Monitor for any services still using the old secret

Many OAuth providers support having multiple active secrets simultaneously during rotation.

Can I use Client Credentials for user-specific operations?

No, Client Credentials flow does not provide user context. The access token represents the client application itself, not any specific user. For user-specific operations, use Authorization Code Flow with PKCE or another user-authentication flow.

How should I handle token expiration in long-running processes?

Implement token caching with expiration awareness:

  • Cache the token and its expiration time
  • Request a new token before the current one expires (use a buffer period)
  • Implement retry logic for token requests
  • Use circuit breakers to handle authorization server outages

What's the difference between client_secret_basic and private_key_jwt?

client_secret_basic uses symmetric authentication with HTTP Basic Auth (client_id:client_secret). private_key_jwt uses asymmetric cryptography where the client signs a JWT with its private key.

Private Key JWT is more secure because:

  • The private key never leaves the client
  • Compromise of the authorization server doesn't expose client credentials
  • Supports non-repudiation
  • Better for high-security environments

How do I implement rate limiting for Client Credentials requests?

Implement rate limiting at multiple levels:

  1. Token caching: Reduce requests by caching tokens
  2. Client-side throttling: Limit token requests per time period
  3. Server-side limits: Authorization servers should enforce per-client rate limits
  4. Exponential backoff: Handle rate limit responses appropriately

Have questions about this topic?

We are happy to discuss your specific needs. Whether you need architecture advice, implementation guidance, or just want to explore possibilities.

Let's Talk
Follow us
All Rights Reserved
© 2011-2026
Progressive Innovation
LAB