Yes, I have done something like this. The context I had was to (attempt to) integrate with the Google Smart Device Management ecosystem. I'm not sure I recommend it, but I was at least successful in using refresh tokens for connecting to the API.
There are two ways I can think of for accomplishing this:
- Implement the
ReactiveOAuth2AuthorizedClientService class yourself
- Use in-memory implementations and load a
refreshToken on startup
Both approaches are similar, because in either case we don't have an accessToken and must give the framework an expired token, so it knows to load a new one on first use.
I went with the second option, though in hindsight the first option could be nicer.
Here is a class that ties all the concepts together and allows you to create a WebClient for either request-based access or for a background service that is not responding to user requests (e.g. a scheduled batch job):
@Component
public class WebClientFactory {
private final ReactiveClientRegistrationRepository clientRegistrationRepository;
private final ServerOAuth2AuthorizedClientRepository authorizedClientRepository;
private final ReactiveOAuth2AuthorizedClientService authorizedClientService;
private final ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider;
private final String baseUrl;
public WebClientFactory(
ReactiveClientRegistrationRepository clientRegistrationRepository,
ServerOAuth2AuthorizedClientRepository authorizedClientRepository,
ReactiveOAuth2AuthorizedClientService authorizedClientService,
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider,
@Value("${my.base-url}") String baseUrl) {
this.clientRegistrationRepository = clientRegistrationRepository;
this.authorizedClientRepository = authorizedClientRepository;
this.authorizedClientService = authorizedClientService;
this.authorizedClientProvider = authorizedClientProvider;
this.baseUrl = baseUrl;
}
public Mono<Void> saveRefreshToken(String registrationId, String principalName, String token) {
var now = Instant.now();
var accessToken = new OAuth2AccessToken(
OAuth2AccessToken.TokenType.BEARER, "none", now.minusSeconds(2), now.minusSeconds(1));
var refreshToken = new OAuth2RefreshToken(token, now);
var principal = new BearerTokenAuthenticationToken(principalName);
principal.setAuthenticated(true);
return this.clientRegistrationRepository.findByRegistrationId(registrationId)
.switchIfEmpty(Mono.error(new InternalAuthenticationServiceException("Unable to find registrationId " + registrationId)))
.map((clientRegistration) -> new OAuth2AuthorizedClient(clientRegistration, principalName, accessToken, refreshToken))
.flatMap((authorizedClient) -> this.authorizedClientService.saveAuthorizedClient(authorizedClient, principal));
}
public WebClient createRequestClient() {
var authorizedClientManager = new DefaultReactiveOAuth2AuthorizedClientManager(
this.clientRegistrationRepository, this.authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(this.authorizedClientProvider);
return createWebClient(authorizedClientManager);
}
public WebClient createBackgroundClient() {
var authorizedClientManager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
this.clientRegistrationRepository, this.authorizedClientService);
authorizedClientManager.setAuthorizedClientProvider(this.authorizedClientProvider);
return createWebClient(authorizedClientManager);
}
private WebClient createWebClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) {
var oauth2 = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
return WebClient.builder()
.filter(oauth2)
.baseUrl(this.baseUrl)
.build();
}
}
Note: I'm re-using the BearerTokenAuthenticationToken from spring-security-oauth2-resource-server for simplicity, as some kind of principal is required to uniquely identify the refresh token.
Spring Boot will provide most of the injected beans for you (defaulting to in-memory), but you will need to provide ReactiveOAuth2AuthorizedClientProvider:
@Bean
public ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider() {
return ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
.refreshToken()
.build();
}
You'll need to set up the same BearerTokenAuthenticationToken as a principal yourself through the ReactiveSecurityContextHolder if using the WebClient in a background service.
See the docs for more info.