How do you get an access token inside a controller using @RegisteredOAuth2AuthorizedClient, and what happens under the hood?
answer
- @RegisteredOAuth2AuthorizedClient("id") OAuth2AuthorizedClient
- OAuth2AuthorizedClientArgumentResolver -> Manager
- Manager -> Providers -> save to Repository
- clockSkew 60s proactive refresh
- auth_code with no client -> redirect (ClientAuthorizationRequiredException)
basics
~10 sAdd an OAuth2AuthorizedClient parameter annotated @RegisteredOAuth2AuthorizedClient("registration-id") to your controller method. Spring resolves it, authorizing (or refreshing) as needed, and you read client.getAccessToken().getTokenValue().
solid answer
~40 sYou declare a controller method parameter `@RegisteredOAuth2AuthorizedClient("my-api") OAuth2AuthorizedClient client`. An argument resolver — `OAuth2AuthorizedClientArgumentResolver` — intercepts it and calls the configured `OAuth2AuthorizedClientManager`. The manager looks up an existing authorized client for that registrationId + current principal via the `OAuth2AuthorizedClientRepository`; if none exists (or the token is expired and refreshable) it runs the appropriate `OAuth2AuthorizedClientProvider` to authorize/re-authorize, stores the result, and hands you the `OAuth2AuthorizedClient`. You then use `client.getAccessToken().getTokenValue()` for the downstream `Authorization: Bearer` header. Note: for authorization_code, if the user has not yet consented, resolution can trigger a redirect to the provider rather than returning a client synchronously — so the annotation is most seamless for client_credentials or already-authorized users.
code
java · 17 lines@RestController
class ApiController {
private final RestClient rest = RestClient.create();
@GetMapping("/orders")
List<Order> orders(
@RegisteredOAuth2AuthorizedClient("my-api") OAuth2AuthorizedClient client) {
String token = client.getAccessToken().getTokenValue();
return rest.get()
.uri("https://api.example.com/orders")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
.retrieve()
.body(new ParameterizedTypeReference<>() {});
}
}go deeper
Know the annotation exists and yields an OAuth2AuthorizedClient whose token you send downstream.
Explain the resolver→manager→provider→repository chain and the refresh behaviour.
Contrast synchronous return for client_credentials vs redirect for authorization_code; mention WebClient/RestClient auto-attach alternatives.
Reason about clockSkew tuning, thread-context requirements of the servlet manager, and when to bypass the annotation for a service-layer manager.
## The annotation **`@RegisteredOAuth2AuthorizedClient`** is a Spring MVC/WebFlux method-parameter annotation. You put it on a parameter of type **`OAuth2AuthorizedClient`** and give it the **registrationId** of the client you want: ```java @GetMapping("/data") String data(@RegisteredOAuth2AuthorizedClient("my-api") OAuth2AuthorizedClient client) { … } ``` Omitting the id makes Spring infer it from the current `OAuth2AuthenticationToken` (only works when the user logged in via that same provider). ## The resolution chain At request time **`OAuth2AuthorizedClientArgumentResolver`** handles the parameter. It builds an `OAuth2AuthorizeRequest` (registrationId + the current `Authentication` principal + the `HttpServletRequest`) and passes it to the **`OAuth2AuthorizedClientManager`** (default `DefaultOAuth2AuthorizedClientManager`). The manager: 1. Loads any existing `OAuth2AuthorizedClient` from the **`OAuth2AuthorizedClientRepository`** for (registrationId, principalName). 2. Delegates to a chain of **`OAuth2AuthorizedClientProvider`s**. Each provider decides whether it should act: `client_credentials`/`authorization_code` provide an *initial* token; `refresh_token` re-authorizes an *existing but expired* one. 3. If a provider returns a new/updated client, the manager **saves** it back to the repository and returns it. 4. If everything is still valid, it returns the existing client unchanged. ## Token freshness and clock skew Providers judge expiry with a **`clockSkew`** (default 60 seconds) so a token about to expire is refreshed proactively. With a refresh token present, `RefreshTokenOAuth2AuthorizedClientProvider` swaps it for a fresh access token transparently — your controller code never changes. ## The authorization_code caveat For the authorization_code grant, if there is no stored client yet, the flow needs a browser redirect to the provider for user consent. The argument resolver signals this by throwing `ClientAuthorizationRequiredException`, which the `OAuth2AuthorizationRequestRedirectFilter` turns into a redirect. So the annotation returns a client *synchronously* only when one already exists or when the grant (client_credentials, refresh_token) needs no user interaction. ## Alternatives Outside a controller (e.g. a service or scheduled job) you inject the `OAuth2AuthorizedClientManager` (or `OAuth2AuthorizedClientService`) directly and call `authorize(...)` yourself. For HTTP calls, the `ServletOAuth2AuthorizedClientExchangeFilterFunction` (WebClient) or `OAuth2ClientHttpRequestInterceptor` (RestClient) can attach the token automatically instead. ## When to use Use the annotation for the common case: a controller needs a token to call a downstream API for the current request/principal.
- What happens if the registrationId uses authorization_code and no token is stored yet?The argument resolver raises ClientAuthorizationRequiredException; the redirect filter sends the user to the provider for consent. It cannot return a client synchronously without user interaction — unlike client_credentials.
- How would you get the token in a non-web background job instead?Inject an OAuth2AuthorizedClientManager (typically AuthorizedClientServiceOAuth2AuthorizedClientManager) and call manager.authorize(OAuth2AuthorizeRequest…), since there is no HttpServletRequest/request context available.
saying these in an interview costs you the question
- Thinking the annotation always triggers a browser redirect.
- Believing the controller method performs the token exchange itself.
- Assuming it works in a background thread with no request context (the servlet manager needs one).