M2M authentication

Client credentials flow in OAuth 2.0: How it works

Hrishikesh Premkumar
CONTENTS

This guide breaks down how the flow works, when to use it, how to implement it securely, and the trade-offs to be aware of. It also covers the flow’s practical use cases and implementation with Node.js examples while showing its internal structure.

What is the Client Credentials Flow?

Imagine a reporting service that pulls sales data every hour from a billing API to generate internal dashboards. There’s no user logging in—just one interaction from one backend to another, which is exactly what Client Credentials Flow is built for.

Client Credentials Flow powers secure, non-interactive communication between backend systems. It’s designed specifically for M2M authentication, where no human is present to click “Allow” or log in.

In today’s service-driven architectures, backend APIs, headless services, and automation tools often need to communicate securely. Client Credentials Flow enables that by letting one system authenticate using its own credentials—kind of like an API key, but with better control and built-in expiration.This flow is part of the broader OAuth 2.0 spec designed for varied use cases.

Here’s how it fits in comparison:

Flow Type
Involves User?
Use Case Example
Authorization Code
✅ Yes
User logging into a web or mobile app
Device Code
✅ Yes
Login on smart TVs or IoT devices
Implicit
✅ Yes
Frontend-only SPAs (now deprecated)
Client Credentials
❌ No
Backend apps, CI tools, microservices

Common scenarios where this flow fits

  • B2B APIs exposing endpoints to partner apps: A logistics platform offering APIs to retail partners, like inventory apps. When the partner app wants to check delivery status or update stock in real time, it authenticates using the Client Credentials Flow—no user login required.
  • Internal services making authenticated API calls to other services: Suppose a data enrichment service inside your company queries a user analytics API every hour to improve targeting. Since both services live within your infrastructure, they use client credentials to talk securely without user prompts.
  • Admin-level API access for system integrations: A headless SaaS product connects with external billing or CRM systems. It fetches nightly usage data using client credentials issued at the organization level, ensuring secure access without relying on a specific user account or session.

Now that you know what the Client Credentials Flow is and where it typically fits, the next question is: When should you actually use it and when should you not?

When and why use Client Credentials Flow?

Client Credentials Flow is built for apps authenticating as themselves—not on behalf of users. It’s ideal when services need direct, non-interactive access to APIs or protected resources, especially in infrastructure or automation-heavy setups.

Use this flow when there’s no human involved and the application can be fully trusted. This includes backends calling each other, cron jobs interacting with APIs, or CI/CD pipelines triggering deployments securely.

Why choose this flow?

  • Simple to implement: no browser redirects or consent screens
  • Lightweight: token retrieval is quick and predictable
  • Secure: In Client Credentials Flow, tokens are scoped, short-lived, and issued specifically to the client application, not a user. This means the client_id represents the authenticated entity, and tokens typically include a sub (subject) claim that matches this client_id. Unlike user-centric flows, there's no user identity involved—access is granted solely based on the client’s credentials and permissions assigned during registration. This makes it well-suited for trusted backend apps, while reinforcing the importance of secure credential handling.

But it comes with trade-offs

The biggest limitation is the lack of user-level granularity. Since tokens are issued to the client, all requests use the same static scope. That means:

  • Static scopes: Since there's no user involved, you can't vary permissions based on who's logged in. Every request carries the same set of scopes — for example, read:all_data or write:logs—regardless of what data is being accessed.
  • No role-based access control (RBAC): You can't check roles like admin, manager, or viewer, because there's no user_id in the token payload. That removes the ability to conditionally expose endpoints or fields

Step-by-step breakdown: How Client Credentials Flow works

This section walks through a real-world example of how a B2B service uses the Client Credentials Flow to authenticate with an API and access protected resources without any end-user involvement.

Step 1: Register the client

The client registers with the authorization server to obtain credentials.

  • A service (e.g., an order-processing backend) needs to authenticate with an API (e.g., inventory service).
  • The authorization server issues:
    • A client_id (public identifier)
    • A client_secret (private key, must be kept secure)
  • Scopes are defined at this point (e.g., inventory:read, inventory:write) to control what the client can access. They are not just labels, they're used by the authorization server to determine which access tokens to issue and by the resource server to enforce access decisions
{  "client_id": "inventory-client-abc123", "client_secret": "secret-value-xyz456", "scope": "inventory:read inventory:write" }

Step 2: Generate an access token

To get an access token, the client sends its credentials to the token endpoint.

  • The client issues a POST request to the authorization server's token endpoint.
  • Required parameters:
    • grant_type=client_credentials
    • client_id and client_secret
    • Optional: scope (if different from default)

Sample cURL command

curl --request POST \ --url https://auth.example.com/oauth2/token \  --header 'Content-Type: application/x-www-form-urlencoded' \  --data 'grant_type=client_credentials&client_id=inventory-client-abc123&client_secret=secret-value-xyz456'

Sample response

{  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6...", "token_type": "Bearer", "expires_in": 3600, "scope": "inventory:read inventory:write" }

Note: Access tokens are short-lived (usually 1 hour) and scoped. Unlike other OAuth flows, refresh tokens are usually not issued because the client is considered trusted and can securely store its own credentials. When a token expires, the client simply re-authenticates using its client_id and client_secret to obtain a new one. This approach reinforces security by avoiding long-lived tokens and keeps the flow simple and stateless.

Step 3: Access the protected resource

To call a protected API, the client includes the access token in the Authorization header using the Bearer scheme.

GET /internal/reports HTTP/1.1  

Host: api.example.com  

Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...

On the backend, the API validates the token in one of two ways:

  • JWTs (Self-contained tokens): The API verifies the tokens digital signature, expiration, and claims using the public key provided by the auth server.
  • Opaque tokens: The API performs introspection by sending the token to the auth server’s /introspect endpoint to check its validity and retrieve metadata (like scope, client ID, etc).

If the token is valid and has sufficient scope, access is granted. Otherwise, the API responds with an appropriate error (401 Unauthorized, 403 Forbidden, etc.)

Common errors and fixes in Client Credentials Flow

Code implementation (Node.js example with Express)

This section shows how to implement a minimal auth server using Express and the OAuth2-server library.

We'll set up client credentials, configure token issuance, and protect an internal API route using access tokens.

Setting up a basic auth server (using OAuth2-server)

This setup defines the core components needed to issue access tokens based on client credentials.

1. Install dependencies

2. Define Client Credentials

🗂️ config.js

module.exports = { clients: [    {      id: 'client-123', secret: 'secret-abc', grants: ['client_credentials'], scope: 'read:logs write:logs'    }  ] };

3. Initialize OAuth2 server

This creates the OAuth server and connects it with Express routes.

const express = require('express'); const OAuth2Server = require('oauth2-server'); const config = require('./config'); const app = express();app.use(express.urlencoded({ extended: true })); const oauth = new OAuth2Server({ model: require('./model'), // Custom logic for getClient, saveToken, etc. allowBearerTokensInQueryString: true });‍

4. Token endpoint

This route issues access tokens based on valid client credentials.

app.post('/oauth/token', (req, res, next) => {  const request = new OAuth2Server.Request(req);  const response = new OAuth2Server.Response(res);  oauth    .token(request, response)    .then(token => res.json(token))    .catch(err => res.status(err.code || 500).json(err)); });

Purpose: This endpoint accepts a POST with grant_type=client_credentials and returns a scoped access token.

Protecting an API route

This middleware validates the token before allowing access to a protected route.

1. Token validation middleware

const authenticate = (req, res, next) => {  const request = new OAuth2Server.Request(req);  const response = new OAuth2Server.Response(res);  oauth    .authenticate(request, response)    .then(token => {      req.user = token; next();    })    .catch(err => res.status(err.code || 401).json({ error: 'Unauthorized' })); };

2. Secured route example

This route is protected and only accessible with a valid access token.

app.get('/admin/logs', authenticate, (req, res) => {  res.json({ logs: ['System booted', 'New user added', 'Error: Disk full'] }); });

3. Expected behaviors

Condition
Response
✅ With valid token
Returns JSON logs
❌ No token
401 Unauthorized
❌ Invalid/expired token
401 Unauthorized with error message

Try the /admin/logs route using a bearer token returned from /oauth/token.

Testing the Flow end-to-end

This section walks through how to validate your Client Credentials implementation using fundamental tools like Postman, curl, and Insomnia.

Use Postman, curl, or Insomnia to test token generation

Start by simulating the /token request to get an access token.

🛠️ cURL Example

curl -X POST https://auth-server.com/token \  -d 'grant_type=client_credentials' \  -d 'client_id=your-client-id' \  -d 'client_secret=your-client-secret'

You should get a JSON response with an access_token, token_type, and expires_in.

Validate token usage

Make an API request with the token in the Authorization header:

curl https://api.yourservice.com/internal/reports \  -H "Authorization: Bearer "

If valid, the API responds with data. You should see an error response if the token is missing or invalid (e.g., 401 Unauthorized).

Simulate token errors

Test how your system handles failures:

Scenario
How to Trigger
Expected Behavior
Expired token
Use a token after its expires_in period
API should return 401 Unauthorized
Invalid client credentials
Send wrong client_id or client_secret
Token endpoint returns 401/400 error
Insufficient scope
Request a token without the needed scope
API should reject the request

Run a mock OAuth 2.0 server

For local testing, simulate the OAuth 2.0 flow with:

  • WireMock (to stub the /token and API endpoints)
  • Express middleware (to simulate a minimal auth server)

Download OAuth2_Client_Credentials_Flow.postman_collection.json

Conclusion

The Client Credentials Flow is essential for secure, machine-to-machine communication, sometimes called M2M authentication, in modern architectures—perfect for internal services, B2B APIs, and automation tools.

This guide explored when and why to use it, how the flow works, and how to implement it using Node.js and the OAuth2-server library. We covered token validation methods (JWT and introspection), walked through testing strategies with Postman and curl, and highlighted key security practices like secret storage, token TTLs, and rate limiting. With proper setup, this flow enables robust, userless access while minimizing risk.

FAQs

What is the OAuth2 client credentials flow?

The OAuth 2.0 client credentials grant flow permits a web service (confidential client) to authenticate when calling another web service using its own credentials instead of impersonating a user.

What is the difference between client credentials flow and auth code flow?

The main difference between the Client Credentials flow and the Authorization Code flow is that the Client Credentials flow relies on application authorization rather than involving the user. Since there is no user authorization, the flow only interacts with the token endpoint.

What is the difference between OAuth2 login and OAuth2 client?

oauth2Login() will authenticate the user with OAuth2 (or OIDC implementation), populating Spring's Principal with the information from either the JWT or the userInfo endpoint. oauth2Client() won't authenticate the user but will seek permission from the OAuth2 authorization server for the resources (scopes) it needs to accessModern APIs frequently rely on OAuth's Client Credentials Flow to securely handle machine-to-machine (M2M) interactions. This allows backend systems—like CI/CD pipelines, billing systems, or microservices—to communicate directly and automatically without user involvement. Rather than relying on end-user logins, these systems authenticate using their own credentials. This approach is especially common in internal communications, background processes, and partner integrations within modern B2B SaaS platforms.

No items found.
Ship Enterprise Auth in days

Acquire enterprise customers with zero upfront cost

Every feature unlocked. No hidden fees.
Start Free
$0
/ month
3 FREE SSO/SCIM connections
Built-in multi-tenancy and organizations
SAML, OIDC based SSO
SCIM provisioning for users, groups
Unlimited users
Unlimited social logins