skip to content

How does automatic access-token refresh work in Spring Security's OAuth2 client, and what are its limits?

level: seniorimportance: should knowfreq 40%

answer

  1. RefreshTokenOAuth2AuthorizedClientProvider, grant_type=refresh_token
  2. needs existing client + refresh token + expired access (clockSkew 60s)
  3. refresh-only, never initial authorization
  4. rotation -> must persist new refresh token (Jdbc in clusters)
  5. no refresh token -> full re-authorization / redirect

basics

~20 s

If an OAuth2AuthorizedClient has a refresh token and its access token is expired, the RefreshTokenOAuth2AuthorizedClientProvider trades the refresh token for a fresh access token automatically when you next request the client. It never does the initial login.

solid answer

~40 s

Add `.refreshToken()` to your `OAuth2AuthorizedClientProviderBuilder` (Spring Boot wires it by default). When the manager is asked for a client whose `OAuth2AccessToken` is expired (judged with a 60-second `clockSkew`) and an `OAuth2RefreshToken` is present, the `RefreshTokenOAuth2AuthorizedClientProvider` POSTs `grant_type=refresh_token` to the token endpoint, receives a new access token (and possibly a rotated refresh token), saves the updated `OAuth2AuthorizedClient`, and returns it. It happens transparently on the next `@RegisteredOAuth2AuthorizedClient` resolution or `manager.authorize(...)` call. Limits: it only *re-authorizes* an existing client — it cannot perform the initial authorization_code grant; if no refresh token exists or refresh fails, you fall back to re-authorizing (redirect for auth_code, fresh request for client_credentials). Some providers rotate refresh tokens, so the store must persist the new one.

code

java · 19 lines
java
@Bean
OAuth2AuthorizedClientManager authorizedClientManager(
        ClientRegistrationRepository registrations,
        OAuth2AuthorizedClientRepository clientRepo) {

    OAuth2AuthorizedClientProvider providers =
        OAuth2AuthorizedClientProviderBuilder.builder()
            .authorizationCode()   // obtains the INITIAL token (+ refresh token)
            .refreshToken()        // silently renews expired access tokens
            .clientCredentials()
            .build();

    DefaultOAuth2AuthorizedClientManager manager =
        new DefaultOAuth2AuthorizedClientManager(registrations, clientRepo);
    manager.setAuthorizedClientProvider(providers);
    return manager;
}
// On the next @RegisteredOAuth2AuthorizedClient resolution, an expired access
// token with a present refresh token is swapped for a fresh one automatically.

go deeper

for a junior

Know refresh happens automatically when the access token expires and a refresh token exists.

for a middle

Name the provider, the clockSkew, and that it only renews, never does initial login.

for a senior

Explain rotation persistence, provider-chain composition, and failure fallback.

for a principal

Address shared JDBC storage for rotation across a cluster, clockSkew tuning, and scope requirements for issuing refresh tokens.

## The problem it solves Access tokens are short-lived. Rather than forcing the user to log in again, OAuth2 issues a longer-lived **refresh token** that the client exchanges for a new access token. Spring automates this. ## The provider **`RefreshTokenOAuth2AuthorizedClientProvider`** is one link in the manager's provider chain. On each `authorize` it checks the existing `OAuth2AuthorizedClient`: - Is there an `OAuth2AccessToken` and is it **expired** (with **`clockSkew`**, default 60 s, so it renews slightly early)? - Is there a non-null **`OAuth2RefreshToken`**? If both hold, it uses a **`DefaultRefreshTokenTokenResponseClient`** to POST `grant_type=refresh_token&refresh_token=…` (plus client authentication) to the **token-uri**. The response provides a fresh `OAuth2AccessToken`; if the provider returns a new refresh token, that replaces the old one (**refresh-token rotation**). The manager's success handler then **saves** the updated client back to the Service/Repository. If the access token is still valid, the provider returns `null` (no-op) and the existing client is used as-is. ## How you trigger it You don't call it directly. It runs whenever the client is requested: - via `@RegisteredOAuth2AuthorizedClient("id")` in a controller, - via `manager.authorize(OAuth2AuthorizeRequest…)` in a service/job, - via the WebClient `ServletOAuth2AuthorizedClientExchangeFilterFunction` / RestClient interceptor before an HTTP call. Spring Boot's default `OAuth2AuthorizedClientManager` already includes the refresh provider, so for most apps it 'just works'. ## Composition With `OAuth2AuthorizedClientProviderBuilder.builder().authorizationCode().refreshToken().clientCredentials().build()`, the chain tries each provider in order. Typically: authorization_code obtains the initial token (with a refresh token if the provider grants one and `offline_access`/appropriate scope was requested), then refreshToken keeps it fresh thereafter. ## Important limits and gotchas - **Refresh only, never initial.** `RefreshTokenOAuth2AuthorizedClientProvider` cannot obtain the *first* token — there must already be a stored client with a refresh token. Initial authorization is authorization_code (user redirect) or client_credentials. - **No refresh token → no refresh.** Many providers only issue refresh tokens when you request the right scope (e.g. `offline_access`) or with specific settings. Without one, an expired access token forces full re-authorization. - **Rotation persistence.** If the IdP rotates refresh tokens, you must persist the new one; a broken/immutable store means the next refresh uses a stale (already-invalidated) token and fails. Use `JdbcOAuth2AuthorizedClientService` in clusters so all nodes see the rotated token. - **Refresh failure** (expired/revoked refresh token) raises an authorization exception; for auth_code that surfaces as a redirect to re-consent. - **clockSkew tuning:** shorten if downstream is strict; lengthen to avoid using tokens about to expire mid-call. - **client_credentials rarely needs it** — it just re-requests a token; refresh tokens are mainly an authorization_code concern. ## When to rely on it Enable and depend on it for user-delegated API access (authorization_code) so long-running sessions don't force repeated logins. Ensure a persistent, shared token store if you run multiple instances.

  • Why might refresh silently never happen even though you added .refreshToken()?
    Because the stored OAuth2AuthorizedClient has no OAuth2RefreshToken — the IdP didn't issue one (often needs offline_access scope or specific config). The provider requires a refresh token to act.
  • What breaks refresh in a multi-instance deployment with token rotation?
    An in-memory per-node store: node A rotates the refresh token but node B still holds the old, now-invalid one. Use JdbcOAuth2AuthorizedClientService so all instances share the rotated token.

saying these in an interview costs you the question

  • Thinking the refresh provider can perform the initial authorization.
  • Assuming refresh works without the IdP actually issuing a refresh token.
  • Ignoring refresh-token rotation, causing stale-token failures.
  • Using in-memory storage in a cluster with rotation enabled.

context