spring-bootspring-securityspring-webflux

Spring Security Reactive OAuth2 Client: Options for Customizing Refresh Endpoint


I have a situation where I'm trying to access a protected resource via OAuth2 Auth Code flow and the authorization server makes refresh tokens available during the initial access token request. The problem arises when trying to make use of these refresh tokens as the authorization server has a distinct refresh endpoint from the token endpoint.

Within a Spring context, that means that this configuration is incomplete:

spring.security.oauth2.client.provider.test.authorization-uri=http://localhost:8085/oauth/authorize
spring.security.oauth2.client.provider.test.token-uri=http://localhost:8085/oauth/token

The missing part is a place where I can defined a "refresh-uri".

Upon review of the OAuth2 RFC, this use of a distinct refresh endpoint appears to go against the standard. This is clearly why no such "refresh-uri" configuration exists in Spring Security. Unfortunately, I have no control over the authorization server I need to interact with so I need to find a method to change the target URI in refresh contexts on the client side.

Initial Workaround Attempts

I first consulted this documentation for customizing refresh token requests but it appears that the main customization options only allow for modification of parameters and headers.

There is a more freeform approach suggested that allows for using a custom WebClient but this also appears to be insufficient as the eventual call to the WebClient will make use of the uri defined in the client's provider configuration, as is the case in this class AbstractWebClientReactiveOAuth2AccessTokenResponseClient.java

This leads into my next thought for a workaround which involves trying to modify the OAuth2RefreshTokenGrantRequest in flight to override the client provider configuration token uri value.

Still, I get the sense that I'm missing some sort of obvious approach for something as simple as modifying the target endpoint in refresh contexts. Any suggestions are greatly appreciated!

Relevant dependency versions:


Solution

  • I think I have found an elegant solution to the problem by decorating the refresh token response client replacing the configured client registration with the client registration for the refresh token:

    @RequiredArgsConstructor
    public class WebClientReactiveRefreshTokenWithSeparateClientRegistrationTokenResponseClient
            implements ReactiveOAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> {
    
        private final ReactiveOAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> delegate;
        private final ClientRegistration refreshTokenClientRegistration;
    
        @Override
        public Mono<OAuth2AccessTokenResponse> getTokenResponse(OAuth2RefreshTokenGrantRequest authorizationGrantRequest) {
            val newRequest = new OAuth2RefreshTokenGrantRequest(
                    refreshTokenClientRegistration,
                    authorizationGrantRequest.getAccessToken(),
                    authorizationGrantRequest.getRefreshToken(),
                    authorizationGrantRequest.getScopes());
            return delegate.getTokenResponse(newRequest);
        }
    }
    

    You can register the refresh token client configuration in your application.yml as a separate client configuration and use it to create the custom client as part of the AuthorizedClientManager configuration for example:

    public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
            ReactiveClientRegistrationRepository clientRegistrationRepository,
            ReactiveOAuth2AuthorizedClientService authorizedClientService
    ) {
        ...
        var refreshTokenClient = new WebClientReactiveRefreshTokenTokenResponseClient();
        val refreshTokenReg = clientRegistrationRepository
                .findByRegistrationId(TOKEN_REFRESH_SERVER_CONFIG_NAME)
                .block();
        val refreshTokenClientWithSeparateReg =
                new WebClientReactiveRefreshTokenWithSeparateClientRegistrationTokenResponseClient(
                        refreshTokenClient, refreshTokenReg);
    
        val authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
                .refreshToken((token) -> token.accessTokenResponseClient(refreshTokenClientWithSeparateReg))
                ...
                .build()