Client credentials and service-to-service auth
The grant with no user, and the one people misuse most.
Open this lesson in the learning hubKey points
- The client credentials grant has no user at all. A service authenticates as itself and receives a token representing the service, not a person.
- Use it for scheduled jobs, service-to-service calls and anything where there is no human to consent. There is no redirect, no browser and no consent screen.
- Never use it to act on behalf of a user. A token with no user identity cannot express "this user request" - and using one for that loses all attribution.
- Scope it narrowly per service. A shared token with broad scope means any compromised service has every permission the fleet has.
- Cache the token until close to expiry rather than fetching one per call. A token endpoint hit on every request is both slow and a load problem for the identity provider.
- Where the caller does act for a user, use token exchange: present the user token and receive one scoped to the next service, preserving identity along the chain.
Example
# No user, no browser, no redirect.
$ curl -X POST https://auth.example.com/oauth/token \
-d grant_type=client_credentials \
-d client_id=reporting-service \
-d client_secret=$SECRET \
-d scope=orders.read
{ "access_token": "eyJ...", "token_type": "Bearer", "expires_in": 3600 }
# The token subject is the SERVICE. There is no user in it.
---
// Spring: cache it, and let the framework refresh near expiry.
@Bean
OAuth2AuthorizedClientManager clientManager(
ClientRegistrationRepository registrations,
OAuth2AuthorizedClientService clients) {
var provider = OAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.build();
var manager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(
registrations, clients);
manager.setAuthorizedClientProvider(provider);
return manager; // caches the token; refreshes when it is near expiry
}
@Bean
RestClient apiClient(OAuth2AuthorizedClientManager manager) {
var interceptor = new OAuth2ClientHttpRequestInterceptor(manager);
interceptor.setClientRegistrationIdResolver(r -> "orders-api");
return RestClient.builder()
.baseUrl("https://orders.internal")
.requestInterceptor(interceptor)
.build();
}
/*
* CHOOSING THE GRANT:
*
* a user is present, browser authorization_code + PKCE
* a user is present, no browser device_code (TVs, CLIs)
* NO user - a job or a service client_credentials
* acting FOR a user, downstream token exchange, not client_credentials
*
* password grant DEPRECATED - do not use it
* implicit grant DEPRECATED - superseded by PKCE
*/
Client credentials identifies a service and cannot represent a user - reach for token exchange when identity must travel downstream.
This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the OAuth 2.0 Course course, and every lesson in it is listed on the OAuth 2.0 Course contents page.