Definition
- Client Credentials flow – the OAuth2 grant used for machine-to-machine authentication where there is no human user and no delegated access.
- Confidential client – a backend service that authenticates with its own
client_idandclient_secretat the token endpoint. - Service token – the issued access token represents the service itself, not a user; it carries the service's own scopes.
- No user involvement – there is no authorization code, no redirect, no consent screen, and no refresh token.
- Least privilege – scopes must be limited to exactly what the service needs.
The National Bank of India settlement service needs to call the fraud-check service at 2am when no human is around. There is no customer to log in. So the settlement service acts like a staff member with its own employee badge: it shows its own service id and service password to the security office (Authorization Server), receives a work pass (token) stamped with exactly the tasks it is allowed to perform, and uses that pass to enter the fraud-check department. No customer identity is involved: the badge belongs to the service itself.
Understanding the Concepts
What is the Client Credentials flow?
The Client Credentials flow is the simplest OAuth2 grant. It is a single back-channel call: the client sends its own credentials to the token endpoint and receives an access token. There is no user, no redirect, no consent screen, and no authorization code.
This flow is used for machine-to-machine communication: cron jobs, batch processes, backend services calling other backend services.
POST /oauth2/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)
grant_type=client_credentials&scope=fraud:check
Response:
{
"access_token": "eyJhbGciOi...",
"token_type": "bearer",
"expires_in": 3600,
"scope": "fraud:check"
}
How do you implement it in Spring?
Spring Security's OAuth2 client support handles Client Credentials with WebClient or RestClient that automatically fetches and caches the token.
package com.bank.service;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FraudCheckClientConfig {
@Bean
public WebClient fraudCheckWebClient(
OAuth2AuthorizedClientManager clientManager) {
var filter = new ServletOAuth2AuthorizedClientExchangeFilterFunction(
clientManager);
filter.setDefaultClientRegistrationId("fraud-service");
return WebClient.builder()
.baseUrl("https://fraud.nationalbank.in")
.apply(filter.oauth2Configuration())
.build();
}
}
# application.yml
spring:
security:
oauth2:
client:
registration:
fraud-service:
client-id: settlement-service
client-secret: ${FRAUD_CLIENT_SECRET}
authorization-grant-type: client_credentials
scope: fraud:check
provider:
fraud-service:
token-uri: https://auth.nationalbank.in/oauth2/token
How does the Resource Server enforce scopes?
The Resource Server validates the token and checks that it carries the required scope. The settlement service can only call endpoints protected by the fraud:check scope.
package com.bank.fraud;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class FraudCheckController {
@PostMapping("/api/v1/fraud/check")
@PreAuthorize("hasAuthority('SCOPE_fraud:check')")
public FraudResult checkTransaction(@RequestBody TransactionRequest request) {
System.out.println("Checking fraud for amount: " + request.getAmount());
return new FraudResult(false, "Transaction appears legitimate.");
}
@PostMapping("/api/v1/accounts/close")
@PreAuthorize("hasAuthority('SCOPE_account:close')")
public String closeAccount(@RequestBody String accountId) {
return "Account " + accountId + " closed.";
}
}
Settlement service (scope=fraud:check):
POST /api/v1/fraud/check --> 200 OK (scope matches)
POST /api/v1/accounts/close --> 403 Forbidden (scope missing)
How is Client Credentials different from Authorization Code?
| Aspect | Client Credentials | Authorization Code |
|---|---|---|
| User involved | No | Yes |
| Token represents | The service itself | The user (delegated) |
| Consent screen | No | Yes |
| Redirect needed | No | Yes |
| Refresh token | No | Yes |
| Use case | Backend-to-backend, cron jobs | Web app login, user-facing |
| Channel | Back-channel only | Front + back channel |
// Client Credentials: service authenticates as itself
package com.bank.service;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.web.client.RestTemplate;
public class ClientCredentialsTokenFetcher {
public String fetchToken() {
RestTemplate rest = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setBasicAuth("settlement-service", "service_secret_123");
var body = new LinkedMultiValueMap<String, String>();
body.add("grant_type", "client_credentials");
body.add("scope", "fraud:check");
var response = rest.postForObject(
"https://auth.nationalbank.in/oauth2/token",
new HttpEntity<>(body, headers), java.util.Map.class);
String token = (String) response.get("access_token");
System.out.println("Token obtained for settlement-service.");
return token;
}
}
Why is there no refresh token?
Refresh tokens exist to keep a user session alive without re-login. In Client Credentials there is no user session. When the token expires, the service simply requests a new one with its own credentials. This is cheap and instant because there is no user interaction.
// When the token expires, just fetch a new one:
public String callFraudApi() {
String token = tokenCache.getOrFetch(); // fetches new if expired
// Use token in Authorization: Bearer header
return webClient.post()
.uri("/api/v1/fraud/check")
.headers(h -> h.setBearerAuth(token))
.retrieve()
.bodyToMono(String.class)
.block();
}
What are common mistakes?
The first mistake is hardcoding the client secret in source code. Secrets must come from a secrets manager like Vault or cloud-native secrets (AWS Secrets Manager, Azure Key Vault) and be rotated regularly.
// WRONG: secret hardcoded
String clientSecret = "my_secret_123"; // in source code!
// CORRECT: secret from environment or secrets manager
String clientSecret = System.getenv("FRAUD_CLIENT_SECRET");
// Or injected via Spring's ${FRAUD_CLIENT_SECRET} property
The second mistake is granting overly broad scopes. The settlement service should only get fraud:check, not account:write or *.
What about higher-assurance alternatives?
For higher security, banks prefer alternatives to a shared client_secret:
- Mutual TLS (mTLS): both the client and server present certificates, so the client authenticates via its certificate rather than a shared secret.
- Signed JWT client assertion: the client signs a JWT with its own private key and sends it as the
client_assertionparameter, proving possession of the key without sharing a secret.
// Signed JWT client assertion (private_key_jwt):
POST /oauth2/token
grant_type=client_credentials
&scope=fraud:check
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion=eyJhbGciOiJSUzI1NiJ9...
// JWT is signed with the client's private key
// Server verifies with the client's registered public key
What should a senior engineer know?
Senior engineers know that Client Credentials tokens should be cached until near-expiry, not fetched on every request, to avoid hammering the Authorization Server. They also know that even internal services must follow least-privilege scoping, and that the flow is only for confidential clients: a browser or mobile app must never use Client Credentials because the secret would be exposed.
Visual Overview
Client Credentials Flow:
┌──────────────────┐ ┌──────────────────┐
│ Settlement │ │ Authorization │
│ Service │ │ Server │
│ │ │ │
│ POST /token │──────►│ Validate: │
│ grant_type= │ │ client_id ✓ │
│ client_credentials│ │ client_secret ✓ │
│ scope=fraud:check│ │ scope allowed ✓ │
│ │◄──────│ │
│ access_token │ │ Issue token │
└────────┬─────────┘ └──────────────────┘
│
│ Bearer token
▼
┌──────────────────┐
│ Fraud Check │
│ Service │
│ │
│ Validate token ✓ │
│ Check scope: │
│ fraud:check ✓ │
│ Return result │
└──────────────────┘
No User Involved:
┌──────────────┐
│ 2:00 AM │ (no human awake)
│ Cron Job │
│ Settlement │
│ Service │
└──────┬───────┘
│
│ Authenticates as ITSELF
│ (client_id + client_secret)
▼
┌──────────────┐
│ Auth Server │ ──► Token with scope=fraud:check
└──────────────┘
Key Takeaway
The Client Credentials flow is the OAuth2 grant for machine-to-machine authentication, where there is no human user and no delegated access. A confidential client, typically a backend service, authenticates directly to the Authorization Server with its own client id and client secret and receives an access token that represents the service itself. The token carries the service's own scopes, and because no user is involved there is no consent screen and no refresh token.
In the National Bank of India project, the overnight settlement batch needs to call the fraud-check service when no customer is logged in. There is no user token to pass, so the settlement service authenticates as itself with its own credentials, requests the fraud:check scope, and receives a short-lived token. It then calls the fraud-check API with that token exactly like any bearer token, and the fraud-check service enforces the scope.
The least-privilege point is central. The settlement service is allowed fraud:check and nothing else, so when it tries to request account:close the Authorization Server refuses at the token endpoint. And when a wrong secret is presented, no token is issued at all. This keeps even trusted internal services confined to exactly what they need.
The nuances to remember are that this flow is only for confidential clients that can protect a secret, never a browser or mobile app, that the secret must come from a secrets manager like Vault and be rotated rather than hardcoded, and that for higher assurance banks often prefer mutual TLS or signed JWT client assertions over a shared secret.
Remember one sentence: Client Credentials lets a service authenticate as itself with its own credentials and receive a scoped token, with no user and no consent, for backend-to-backend calls.
---