Learning the client credentials flow for machine-to-machine communication: recognizing when this flow fits, preparing a confidential client, using service accounts and client roles, and integrating it with backend APIs without user presence.

In episode 5 you learned the authorization code flow, which takes users through the Keycloak login screen, complete with PKCE and redirect URIs. Episode 6 flips the perspective: there's no user behind the screen at all. You'll learn the client credentials flow — the OAuth 2.0 flow for communication between two applications, not between an application and a human. This is the flow most often used when one backend service calls another backend service.
Client credentials is the simplest flow of all the OAuth 2.0 flows:
The analogy: if the authorization code flow is an employee showing their ID card when entering a building, client credentials is the access card of a delivery robot — it doesn't matter who sent it, the important thing is the robot is authenticated.
The flow is brief, just two major steps:
No redirects, no authorization code, no user interaction:
curl -X POST "https://kc.example.com/realms/my-realm/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=payment-service" \
-d "client_secret=4f9a2c8d..."Notice grant_type=client_credentials as the flow marker. Without that parameter, Keycloak doesn't know which flow you're using. The response comes back as JSON:
{
"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6...",
"expires_in": 300,
"token_type": "Bearer",
"scope": "email profile"
}An important note: in this flow Keycloak does not return a refresh_token — unlike flows involving a user. Since there's no user session to maintain, the client simply requests a new token when the old one expires.
Every confidential client created in Keycloak automatically has a service account — a virtual user whose name matches the client ID. When you open the Service account roles tab in the admin console, this virtual user is what the token represents.
Because a service account is a regular user from Keycloak's point of view, you can:
The assigned roles appear in the access token:
{
"sub": "bf3a1c2e-...",
"preferred_username": "service-account-payment-service",
"realm_access": {
"roles": ["view-orders"]
},
"resource_access": {
"payment-service": {
"roles": ["approve-payment"]
}
}
}Here realm_access.roles holds the global roles, while resource_access holds client roles specific to the target service. The target server can check these roles before processing a request — this is the basis of machine-to-machine access control in Keycloak.
To use this flow, the client must be of type confidential — a client that has a secret. The steps in the admin console:
payment-service.Tip
Run a local Keycloak with kc.sh start-dev if you don't have one yet, then open the admin console at http://localhost:8080/admin. The names my-realm and payment-service in this episode are examples used consistently throughout the series.
The secret is confidential. Never put it in frontend code, public repositories, or logs. Store it in an environment variable or a secret manager like Vault, and rotate it periodically via the Credentials tab.
A client secret is just one method. Keycloak supports several ways for a client to prove its identity:
| Method | How It Works | Strength |
|---|---|---|
| Client secret | Secret sent in the Authorization header or form body | Simplest, suitable for internal use |
| Signed JWT | Client signs a JWT assertion with its private key | No secret in transit |
| mTLS | Client uses a TLS certificate for authentication | Strongest, suited for tight perimeters |
The method is chosen via the Client authentication dropdown in the Credentials tab. For most internal cases, a client secret is enough; for cross-organization integrations or certificate-based infrastructure, consider JWT or mTLS.
The most common pattern is a backend service requesting a token, then sending it to the target API as a Bearer token:
TOKEN=$(curl -s -X POST "https://kc.example.com/realms/my-realm/protocol/openid-connect/token" \
-d "grant_type=client_credentials" \
-d "client_id=payment-service" \
-d "client_secret=4f9a2c8d..." | jq -r .access_token)
curl -s -H "Authorization: Bearer $TOKEN" \
"https://api.example.com/orders/pending"Key steps on the target API side: validate the token signature via JWKS, check iss, aud, and exp, then check the roles in realm_access or resource_access before allowing the request.
This flow appears most often in four scenarios:
For scheduled jobs, remember: tokens have a limited lifetime (expires_in). Don't store old tokens on disk; request a new token when you're about to use it.
In this episode (episode 6) you learned the client credentials flow: when to use it, how tokens are requested and used, the role of service accounts and client roles, confidential client configuration, client authentication methods, and an API integration example.
Key takeaways:
resource_access control what a service may do.iss, aud, exp, and roles before processing.In the next episode (episode 7) we go deeper into token management: refresh tokens — how to extend a user's access without asking for their password again, when tokens are rotated, and how to invalidate them when stolen or terminated.