skip to content

How do you configure and use the client_credentials grant for machine-to-machine calls in Spring Security's OAuth2 client?

level: seniorimportance: should knowfreq 50%

answer

  1. authorization-grant-type: client_credentials, no user/redirect
  2. only token-uri needed (no authorization-uri)
  3. ClientCredentialsOAuth2AuthorizedClientProvider, no refresh token
  4. principalName = client-id for M2M
  5. client_secret_basic default; private_key_jwt for stronger auth

basics

~10 s

Register a client with authorization-grant-type: client_credentials (client id, secret, token-uri, scopes). No user is involved — the app authenticates itself. A ClientCredentialsOAuth2AuthorizedClientProvider fetches the token; you attach it to downstream calls.

solid answer

~40 s

client_credentials is the grant for service-to-service (no end user). In `application.yml` you set `spring.security.oauth2.client.registration.<id>.authorization-grant-type: client_credentials` with the client-id, client-secret, scope, and the provider's `token-uri`. Spring's `ClientCredentialsOAuth2AuthorizedClientProvider` POSTs client credentials to the token endpoint and returns an `OAuth2AuthorizedClient` with just an access token (typically no refresh token — you simply re-request when it expires). You obtain it via `@RegisteredOAuth2AuthorizedClient("<id>")`, or in a background job via an `AuthorizedClientServiceOAuth2AuthorizedClientManager`. The `principalName` is usually the client-id itself since there is no user. For WebClient/RestClient you can register the exchange filter / interceptor so the token is attached and refreshed automatically. Client authentication defaults to `client_secret_basic`; you can switch to `client_secret_post` or `private_key_jwt`.

code

java · 20 lines
java
// Auto-attach client_credentials tokens to a WebClient
@Bean
WebClient apiClient(OAuth2AuthorizedClientManager manager) {
    ServletOAuth2AuthorizedClientExchangeFilterFunction oauth =
        new ServletOAuth2AuthorizedClientExchangeFilterFunction(manager);
    oauth.setDefaultClientRegistrationId("my-api");
    return WebClient.builder()
        .apply(oauth.oauth2Configuration())
        .build();
}

// Every call now carries a fresh client_credentials bearer token:
// apiClient.get().uri("https://api.example.com/orders").retrieve()...

// Manual fetch in a @Scheduled job:
OAuth2AuthorizeRequest req = OAuth2AuthorizeRequest
        .withClientRegistrationId("my-api")
        .principal("my-service")   // no user -> use the client id
        .build();
String token = manager.authorize(req).getAccessToken().getTokenValue();

go deeper

for a junior

Know it's the no-user, app-to-app grant configured with id/secret/token-uri.

for a middle

Explain the provider, the absence of a refresh token, and getting the token via the annotation or a manager.

for a senior

Cover client authentication methods, background vs request retrieval, and WebClient/RestClient auto-attach.

for a principal

Discuss token-endpoint load across instances, JDBC caching, private_key_jwt, and secret management/rotation.

## What client_credentials is for The **client_credentials** grant is OAuth2's **machine-to-machine** flow: there is **no end user and no browser redirect**. The client application authenticates *as itself* to the authorization server and receives an access token representing the application, not a person. Use it for backend jobs, service meshes, cron tasks, and internal API calls. ## Configuration ```yaml spring: security: oauth2: client: registration: my-api: provider: my-idp authorization-grant-type: client_credentials client-id: my-service client-secret: ${MY_SECRET} scope: orders.read client-authentication-method: client_secret_basic # default provider: my-idp: token-uri: https://idp.example.com/oauth2/token ``` Note: only `token-uri` is needed for the provider (no authorization-uri, since there is no user redirect). ## The runtime path **`ClientCredentialsOAuth2AuthorizedClientProvider`** handles it. When the manager runs the provider chain and finds no valid token, this provider uses a **`DefaultClientCredentialsTokenResponseClient`** to POST to the token endpoint with the configured client authentication (default **`client_secret_basic`** = HTTP Basic; alternatives `client_secret_post`, or `private_key_jwt` for asymmetric client auth). The response yields an `OAuth2AccessToken`. The resulting `OAuth2AuthorizedClient` usually has **no refresh token** — that's normal for this grant. When the access token expires, the provider simply requests a **new** one. ## Obtaining the token in code - **In a controller / request thread:** `@RegisteredOAuth2AuthorizedClient("my-api") OAuth2AuthorizedClient client`. - **In a background job:** inject an `AuthorizedClientServiceOAuth2AuthorizedClientManager` (context-free) and call `manager.authorize(OAuth2AuthorizeRequest.withClientRegistrationId("my-api").principal("my-service").build())`. Provide a stable **principalName** (commonly the client-id) so the service caches/keys the token consistently. - **Automatic HTTP integration:** register `ServletOAuth2AuthorizedClientExchangeFilterFunction` on a `WebClient`, or the `OAuth2ClientHttpRequestInterceptor` on a `RestClient`, with the default client registration set — the token is fetched, cached, and re-fetched on expiry without manual code. ## Gotchas - **No refresh token** is expected — don't treat its absence as an error; the provider re-authorizes from scratch. - **clockSkew** (60 s default) means a near-expiry token is renewed proactively. - With **in-memory service**, each instance re-fetches its own token; that's usually fine but multiplies token-endpoint traffic — consider caching or JDBC service. - Keep the **client-secret** out of source: use env vars / a secrets manager. - For higher assurance, prefer **private_key_jwt** over a shared secret. - A background manager built by hand must have its provider chain set (`.clientCredentials()`), or it returns null. ## When to use vs alternatives Use client_credentials for app identity. If you need to call an API *on behalf of a signed-in user*, use authorization_code + refresh_token instead — client_credentials cannot represent a user or their scopes.

  • Why is there usually no refresh token with client_credentials?
    Because there is no user session to preserve — the app can always re-authenticate with its own credentials. The provider just requests a brand-new access token when the old one expires.
  • How can you avoid a shared client secret?
    Use client-authentication-method private_key_jwt: the client signs a JWT assertion with its private key instead of sending a secret, so no shared symmetric secret is transmitted.

saying these in an interview costs you the question

  • Expecting a browser redirect or user consent for client_credentials.
  • Treating the missing refresh token as a bug.
  • Configuring authorization-uri for a client_credentials-only registration.
  • Hardcoding the client secret in source or config committed to VCS.

context