Sitemap
ITNEXT

ITNEXT is a platform for IT developers & software engineers to share knowledge, connect, collaborate, learn and experience next-gen technologies.

Understanding OAuth 2.0 Client Credentials Flow: Concepts, Architecture, and How the Flow Works (Part 1)

14 min readOct 14, 2026

--

How applications prove who they are and get permission when there is no human in the loop

In the previous articles of this series, we looked at the basic concepts of Identity and Access Management, SAML Authentication, OAuth 2.0 (Part 1, Part 2 and Part 3 — PKCE) and X.509 Certificates. I strongly recommend going through the OAuth 2.0 articles first, because this article builds on the same actors and vocabulary.

In every OAuth 2.0 flow we have discussed so far, there was always a person somewhere in the picture: a user who opened a browser, typed a password, and clicked “Allow” on a consent screen. The whole Authorization Code flow is built around that human moment.

But think about how much traffic in a modern system has no human at all:

  • An Order Service calls an Inventory Service to reserve stock.
  • A nightly billing job reads usage data from a Metering API.
  • A CI/CD pipeline pushes a build artifact into an Artifact Registry.
  • A partner company’s server pulls daily reports from your API.

Who logs in here? Who clicks “Allow”? Nobody. These are machines talking to machines, and they need a way to prove who they are and what they are allowed to do.

This is exactly the problem the Client Credentials Grant solves. It is one of the grant types defined in the OAuth 2.0 specification (RFC 6749, Section 4.4), and it is by far the most common way services authenticate to each other in the world of APIs and microservices.

Because there is a lot to cover, I have split this topic into three parts:

  • Part 1 (this article): Why machines need their own identity, where Client Credentials fits among the OAuth grants, the actors, client registration, the step-by-step flow, and the actual HTTP messages.
  • Part 2: What’s inside the access token, how APIs validate it, scopes and least privilege, token lifecycle and caching, how popular identity providers implement the flow, code samples, and the use cases, benefits, and limitations of the flow.
  • Part 3: Making it secure in production: client authentication methods (from shared secrets to private_key_jwt and mutual TLS), sender-constrained tokens (mTLS and DPoP), going "secretless" with workload identity federation, secret rotation, a threat model, and a best-practices checklist.

The Problem: Machines Need Identity Too

Let’s start with a simple analogy.

Imagine a large hotel. When a guest arrives, they show their ID at the front desk and get a key card that opens their room, for the length of their stay. That is like a user logging in through the Authorization Code flow.

Now think about the housekeeping department. Housekeeping doesn’t borrow a guest’s key card to clean rooms. Instead, the department has its own staff badge, issued by the hotel’s security office after verifying the staff member. That badge:

  • identifies housekeeping (not any guest),
  • opens only the floors and service rooms housekeeping is allowed into,
  • expires at the end of the shift, and
  • can be revoked centrally by the security office at any time.

The Client Credentials flow is the “staff badge” model for software. The application has its own identity, gets a short-lived, limited-permission pass from a central authority, and uses that pass to open only the doors it is allowed to open.

How it was (and sometimes still is) done before

Before OAuth became common, service-to-service calls were usually protected in one of these ways:

  1. Hard-coded API keys: A long random string is pasted into every caller’s configuration. It usually never expires, is often shared by many services, carries no notion of “what is this caller allowed to do”, and is painful to rotate.
  2. A “service user” with username and password: Someone creates a fake user account (for example svc-billing) and every service sends that password, with HTTP Basic authentication, on every single API call to every single API. Each API then needs to check that password against the user store.
  3. IP allow-listing: “If the call comes from 10.0.3.15, it must be the billing server.” Network location is not identity, and in a cloud or container world, IP addresses change all the time.
  4. Borrowing a real person’s credentials: An integration runs with an employee’s account. When that person leaves the company, the integration breaks, and the audit log says a human did things they never did.

Here is what this “secret sprawl” looks like:

Press enter or click to view image in full size

Every arrow carries a long-lived secret. Every API has to know and check those secrets. If any one of them leaks, an attacker has permanent access, and nobody can easily tell which caller did what.

What we actually want

What we want instead is:

  • Every service has its own identity (its own badge).
  • The long-lived credential is shown to only one trusted party, the Authorization Server, instead of being sprayed to every API.
  • APIs receive short-lived tokens with limited permissions.
  • There is one central place to grant access, revoke access, and audit who did what.
Press enter or click to view image in full size

Now the long-lived credential travels only to the Authorization Server. APIs only ever see short-lived tokens. This is the Client Credentials model.

Where Client Credentials Fits Among OAuth 2.0 Grants

OAuth 2.0 defines several “grant types”, which are simply different ways for a client to obtain an access token. Choosing the right one starts with one question: is the application acting for a user, or for itself?

Press enter or click to view image in full size

A quick note on deprecated grants: You may also have heard of the Implicit grant (we covered its problems in Part 3 — PKCE) and the Resource Owner Password Credentials grant. Both are discouraged by the OAuth 2.0 Security Best Current Practice (RFC 9700) and are dropped from the upcoming OAuth 2.1. Client Credentials, on the other hand, remains a first-class grant in OAuth 2.1.

Here is a side-by-side comparison with the flow we already know:

Remember the front channel and back channel we discussed in OAuth 2.0 Part 3 — PKCE? The Client Credentials flow uses only the back channel. There is no browser, no redirect, no authorization code. That makes it both simpler and, by design, inaccessible to anything running on a user’s device.

The Actors in the Client Credentials Flow

In the classic OAuth 2.0 picture we have four roles: Resource Owner, Client, Authorization Server, and Resource Server.

Get Anirban Bhattacherji’s stories in your inbox

Join Medium for free to get updates from this writer.

In the Client Credentials flow, something interesting happens: the Client and the Resource Owner become the same entity. The application is requesting access to resources that it owns itself, or to resources that an administrator has already decided it may access. There is no separate human who needs to say “yes”.

Press enter or click to view image in full size

Let’s look at each actor:

  1. Client (and Resource Owner): The application that needs to call an API. It could be a microservice, a scheduled job, a daemon, a CI pipeline, or a partner’s backend server. It has its own credentials and it must be a confidential client, meaning it runs somewhere the credentials can be kept secret (a server, a container, a managed cloud function), never on a user’s device.
  2. Authorization Server (AS): The trusted party that knows every registered client. It authenticates the client, checks what that client is allowed to access, and issues short-lived access tokens. Examples include Microsoft Entra ID (Azure AD), Okta, Auth0, Keycloak, AWS Cognito, and Ping Identity.
  3. Resource Server (RS): The API that hosts the protected data or operations. It never sees the client’s long-lived credentials. It only receives access tokens, validates them, and decides whether to serve the request.
  4. Administrator (the “consent” giver): In the user-based flows, the user consents at runtime. In Client Credentials, consent happens ahead of time: an administrator or platform team registers the client and decides which APIs and scopes it may use. In Microsoft Entra ID, for example, this is literally called admin consent for application permissions.

Inside the Client

Just like we broke down the client’s internal components in OAuth 2.0 Part 1, a well-built Client Credentials client usually has these pieces:

Press enter or click to view image in full size
  1. Credential Store: Holds the client’s long-lived credential (a client secret, a private key, or a certificate). Ideally this is a secrets manager or vault, not a config file.
  2. Token Acquirer: Calls the Authorization Server’s token endpoint when a new token is needed.
  3. Token Cache: Keeps the current access token in memory and reuses it until it is close to expiry. This is important, as we will see in Part 2.
  4. Resource Handler: Attaches the token to outgoing API calls and handles errors like 401 Unauthorized.

Notice there is no “Authorization Handler” or “Authorization Grant Processor” like in the Authorization Code flow, because there are no redirects and no authorization codes.

Before the Flow: Client Registration

Just as we saw in the App Registration section of OAuth 2.0 Part 1, a client must be registered with the Authorization Server before it can request tokens. For Client Credentials, the registration typically captures:

  1. Client name: A human-friendly name, for example order-service.
  2. Client ID: A public identifier the AS assigns (or that you choose).
  3. Client credential: A client secret, a public key (or a URL to a JWKS), or a certificate. We will explore these options in detail in Part 3.
  4. Allowed grant types: client_credentials.
  5. Allowed scopes / API permissions: For example inventory.read and inventory.reserve, and the API (audience) they belong to.
  6. Token endpoint authentication method: How the client will prove its identity, for example client_secret_basic or private_key_jwt.
  7. Token lifetime and other policies: For example, a one-hour token lifetime.

Registration can be done through an admin console, a CLI, Infrastructure as Code (for example Terraform), or programmatically through Dynamic Client Registration (RFC 7591):

POST /register HTTP/1.1
Host: auth.example.com
Authorization: Bearer <initial-access-token-for-platform-team>
Content-Type: application/json
{
"client_name": "order-service",
"grant_types": ["client_credentials"],
"token_endpoint_auth_method": "client_secret_basic",
"scope": "inventory.read inventory.reserve"
}
HTTP/1.1 201 Created
Content-Type: application/json

{
"client_id": "order-service",
"client_secret": "s3cr3t-Xy9",
"client_id_issued_at": 1791536000,
"client_secret_expires_at": 1799312000,
"grant_types": ["client_credentials"],
"token_endpoint_auth_method": "client_secret_basic",
"scope": "inventory.read inventory.reserve"
}

A secret as short as s3cr3t-Xy9 is used here only to keep the examples readable. Real client secrets should be long, randomly generated values (for example, 32 bytes or more of randomness).

Here is the full registration and provisioning picture:

Press enter or click to view image in full size

Two good habits are already visible here:

  • The Authorization Server shows the secret only once and stores a hash of it (the same way passwords are stored).
  • The application reads the secret from a vault at runtime, rather than from source code or a configuration file in a Git repository.

The Client Credentials Flow, Step by Step

Now let’s walk through the actual flow. Our running example: the Order Service needs to check and reserve stock in the Inventory API.

Press enter or click to view image in full size

Let’s understand each step:

  1. Request a token: The client sends a POST request to the Authorization Server's token endpoint. It says "I want a token using the client_credentials grant", optionally lists the scopes it needs, and proves its identity (for example, with its client ID and secret).
  2. Authenticate the client: The Authorization Server checks that the client really is who it claims to be. With a client secret, it hashes the presented secret and compares it to the stored hash. With stronger methods (covered in Part 3), it verifies a signature or a certificate.
  3. Authorize the request: The Authorization Server checks that: a) this client is allowed to use the client_credentials grant, b) every requested scope is one this client has been granted, and c) the client is active (not disabled or expired).
  4. If the client didn’t ask for any scope, the AS may apply a default set of scopes configured for it, or reject the request.
  5. Issue the token: The Authorization Server creates an access token, usually a signed JWT that is valid for a short time (typically between 5 and 60 minutes).
  6. Return the token: The AS responds with the access token, its type (Bearer), its lifetime in seconds (expires_in), and the granted scopes. No refresh token is returned; we will see why shortly.
  7. Cache the token: The client stores the token in memory and reuses it for subsequent calls. It should not request a new token for every API call.
  8. Call the API: The client calls the Resource Server and puts the token in the Authorization header using the Bearer scheme.
  9. Validate the token: The Resource Server validates the token: is the signature valid, was it issued by the trusted AS, is it meant for this API (audience), has it expired, and does it contain the scope needed for this operation?
  10. Respond: If everything checks out, the Resource Server returns the requested data.
  11. Reuse the token: Subsequent calls reuse the same token until it is close to expiry. Then the client simply goes back to step 1.

That’s it. Compared with the 11 steps of the Authorization Code flow we discussed in OAuth 2.0 Part 2, this is refreshingly simple: one call to get a token, then normal API calls. It’s quite simple, isn’t it?

Looking at the Actual HTTP Messages

Let’s now look at what actually travels over the wire.

Token request

POST /oauth2/token HTTP/1.1
Host: auth.example.com
Authorization: Basic b3JkZXItc2VydmljZTpzM2NyM3QtWHk5
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&scope=inventory.read%20inventory.reserve

Here’s what those parameters mean:

  • grant_type=client_credentials (required) tells the Authorization Server which grant type the client is using.
  • scope (optional) is a space-separated list of the permissions the client wants. It is URL-encoded in the body, which is why the space shows up as %20. A client should ask only for the scopes it needs for the job at hand.
  • Authorization: Basic ... carries the client's credentials. This method is called client_secret_basic.

Some Authorization Servers also accept a parameter that says which API the token is for. The standard way is the resource parameter from RFC 8707 (Resource Indicators). Some vendors use their own parameter instead, such as audience in Auth0. We will come back to why this matters in Part 3.

How is the Basic header built?

The Authorization: Basic value is nothing magical:

Press enter or click to view image in full size

Important: Base64 is an encoding, not encryption. Anyone who sees that header can decode it back to the secret in a second. That is why the token endpoint must be called over HTTPS (TLS). Also, the OAuth 2.0 specification says the client ID and secret should be form-URL-encoded before Base64 encoding. This only matters when they contain special characters, but it’s a common source of “it works in Postman but not in my code” bugs.

The same request using curl:

curl -X POST https://auth.example.com/oauth2/token \
-u "order-service:s3cr3t-Xy9" \
-d "grant_type=client_credentials" \
-d "scope=inventory.read inventory.reserve"

There is another variant called client_secret_post, where client_id and client_secret are sent in the request body instead of the header. The specification says it is not recommended and should be used only by clients that cannot use the Basic header. We will compare all the client authentication methods in Part 3.

Successful token response

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCIsImtpZCI6IjIwMjYtMTAta2V5LTEifQ.eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20i...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "inventory.read inventory.reserve"
}

Here’s what those fields mean:

  • access_token is the token the client will present to the API.
  • token_type is usually Bearer, meaning "whoever bears (holds) this token can use it". Keep that in mind; it's an important security point in Part 3.
  • expires_in is the token lifetime in seconds (here, one hour).
  • scope is the scope that was actually granted. It may be narrower than what was requested, so a well-behaved client checks it.
  • The Cache-Control: no-store and Pragma: no-cache headers tell any proxy or browser in between not to cache this response, because it contains a credential.

Wait, where is the refresh token?

You might remember from OAuth 2.0 Part 2 that the Authorization Code flow often returns a refresh_token. In the Client Credentials flow, the specification says a refresh token should not be included. The reason is simple:

  • A refresh token exists so that a client can get a new access token without bothering the user again.
  • In Client Credentials there is no user to bother. The client can just authenticate again with its own credentials.
  • A refresh token would be one more long-lived credential to protect, with no benefit.

So when the token expires, the client simply repeats the token request.

Error responses from the token endpoint

If something goes wrong, the Authorization Server returns an error in a standard format:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="auth.example.com"
Content-Type: application/json
Cache-Control: no-store
{
"error": "invalid_client",
"error_description": "Client authentication failed"
}

The standard error codes are:

Many Authorization Servers also return 429 Too Many Requests if a client asks for tokens too frequently. This usually means the client isn't caching its tokens.

Here is how an Authorization Server decides which response to send:

Press enter or click to view image in full size

The difference matters to the client: a 401 means "get a new token and try again", while a 403 means "a new token won't help; you are not allowed to do this."

Summary

Let’s recap what we covered in this part:

  • Machines need their own identity. Static API keys, shared service-account passwords, IP allow-lists, and borrowed user accounts all spread long-lived secrets around and make auditing hard.
  • The Client Credentials grant lets an application obtain an access token for itself, with no user involved. The client is also the resource owner, and an administrator gives consent in advance by registering the client and granting it scopes.
  • It’s only for confidential clients (servers, containers, scheduled jobs) and uses only the back channel: no browser, no redirect, no authorization code.
  • Before anything else, the client is registered with the Authorization Server, which records its ID, its credential, the allowed grant type, and its allowed scopes.
  • The flow is just one call to get a token: POST /token with grant_type=client_credentials plus client authentication. After that, the client calls APIs with Authorization: Bearer <token>.
  • No refresh token is issued; the client simply asks again when the token is about to expire.
  • The token endpoint returns standard errors such as invalid_client and invalid_scope. APIs answer with 401 (get a new token) or 403 insufficient_scope (a new token won't help).

In Part 2, we’ll look at the other side of the token: what’s inside it, how APIs validate it, how scopes and audiences enforce least privilege, how to manage and cache tokens properly, how popular identity providers implement the flow, code samples, and finally the use cases, benefits, and limitations of the flow.

Next Read

Understanding OAuth 2.0 Client Credentials Flow: Tokens, Validation, Use Cases, Benefits, and Limitations (Part 2)

--

--

ITNEXT
ITNEXT

Published in ITNEXT

ITNEXT is a platform for IT developers & software engineers to share knowledge, connect, collaborate, learn and experience next-gen technologies.