Describe the bug I made an application with Spring Security OAuth Client for WebClient to make it easier to obtain a token. But unfortunately I have some problems. Basically when the token expires, the token is not being renewed.

I'm not sure if this is a bug, but I couldn't find a solution.

Unfortunately, every time this happens I need to restart the application.

To Reproduce My OAuth2ClientConfig.java:

import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.security.oauth2.client.AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.OAuth2AuthorizationContext;
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProviderBuilder;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import reactor.core.publisher.Mono;

@Configuration
@Profile("local")
public class Oauth2ClientConfigLocal {

    @Bean
    ReactiveClientRegistrationRepository getRegistration(
        @Value("${spring.security.oauth2.client.provider.payment.token-uri}")
        final String tokenUri,
        @Value("${spring.security.oauth2.client.registration.payment.client-id}")
        final String clientId,
        @Value("${spring.security.oauth2.client.registration.payment.client-secret}")
        final String clientSecret,
        @Value("${spring.security.oauth2.client.registration.payment.scopes}")
        final String scope
    ) {
        final ClientRegistration registration = ClientRegistration
                .withRegistrationId(LOCAL_RESOURCE_ID)
                .tokenUri(tokenUri)
                .clientId(clientId)
                .clientSecret(clientSecret)
                .authorizationGrantType(AuthorizationGrantType.PASSWORD)
                .scope(scope)
                .build();
        return new InMemoryReactiveClientRegistrationRepository(registration);
    }

    @Bean
    public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
        final ReactiveClientRegistrationRepository clientRegistrationRepository,
        final ReactiveOAuth2AuthorizedClientService authorizedClientService,
        @Value("${spring.security.oauth2.client.registration.payment.username}") final String username,
        @Value("${spring.security.oauth2.client.registration.payment.password}") final String password) {

        final ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
                ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
                        .password()
                        .build();

        final AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager =
                new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
                        clientRegistrationRepository, authorizedClientService);

        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
        authorizedClientManager.setContextAttributesMapper(contextAttributesMapper(username, password));

        return authorizedClientManager;
    }

    private Function<OAuth2AuthorizeRequest, Mono<Map<String, Object>>> contextAttributesMapper(final String username, final String password) {
        return authorizeRequest -> {
            final Map<String, Object> contextAttributes = new HashMap<>();
            contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username);
            contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password);
            return Mono.just(contextAttributes);
        };
    }

}

My WebClientConfig:

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;

import io.netty.channel.ChannelOption;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
import reactor.netty.http.client.HttpClient;

@Configuration
public class WebClientConfig {  

    private final String activeProfile;

    public WebClientConfig(@Value("${spring.profiles.active}") final String activeProfile) {
        this.activeProfile = activeProfile;
    }

    @Bean(name = OAUTH_WEBCLIENT)
    public WebClient webClient(final ReactiveOAuth2AuthorizedClientManager authorizedClientManager) {
        final String resourceId = activeProfile.equals("local") ? LOCAL_RESOURCE_ID : PAYMENT;


        final HttpClient httpClient  = HttpClient.create()
            .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 30000)
            .doOnConnected(connection -> {
                connection.addHandler(new ReadTimeoutHandler(10))
                    .addHandlerLast(new WriteTimeoutHandler(10));
            });

        final ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
                new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
        oauth.setDefaultClientRegistrationId(resourceId);
        return WebClient.builder()
                .filter(oauth)
                .clientConnector(new ReactorClientHttpConnector(httpClient))
                .build();
    }
}

My call:

public Mono<ResponseEntity<String>> post(final String uri, final String body, final String contentType) {
        return this.webClient.post()
                .uri(uri)
                .header(CONTENT_TYPE, contentType)
                .body(Mono.just(body), String.class)
                .retrieve()
                .onStatus(HttpStatus::isError, response -> onError(response))
                .toEntity(String.class);
 }

Expected behavior Get a new access token just before the expiration of the old one.

Comment From: alexandre99

I was able to find the solution to this problem of mine and now is doing the token renewal. Due to a lack of knowledge, I ended up seeing that the api generated a refresh token and the way it was being coded was preventing the renewal of the token.

Then the solution was to add the refreshtoken when creating the ReactiveOAuth2AuthorizedClientProvider.

Follow the example:

 final ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
                ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
                        .password()
                        .refreshToken()
                        .build();

From the analysis I made debugging the code, I found that when the application returns a refresh token and we don't do this configuration ReactiveOAuth2AuthorizedClientProvider it basically just goes through the authorization flow of the PasswordReactiveOAuth2AuthorizedClientProvider class, in this class there is a verification that the token is expired and that there is a refreshtoken, so oauth2client does not perform the flow to obtain a new token using the username and password.

When configuring the refresh token when creating the ReactiveOAuth2AuthorizedClientProvider, in addition to going through the PasswordReactiveOAuth2AuthorizedClientProvider class, it also goes through the RefreshTokenReactiveOAuth2AuthorizedClientProvider class, where a new access token is requested through the refresh token.

Note I had another problem after I made the configuration described above.

The application I use locally running the oauth2 server to do my tests, it stores the access token and the refresh token in memory, that is, after I have logged in and stopped the oauth2 server and run it again, when I did a new request to obtain the access token through the refresh token, an exception occurred stating that the refresh token was not valid. However, the oauth2 client did not understand that it should re-login again with username and password.

To solve this problem, I added a retry to the WebClient request and filtered for this retry only if it happens to be an authentication error.

@Override
public Mono<ResponseEntity<String>> get(final String uri) {
    return this.webClient.get()
                    .uri(uri)
                    .retrieve()
                    .onStatus(HttpStatus::isError, response -> onError(response))
                    .toEntity(String.class)
                    .retryWhen(Retry.backoff(3l, Duration.ofSeconds(1))
                            .filter(retryFilter));
    }

Comment From: jgrandja

@alexandre99

Regarding your comment:

when I did a new request to obtain the access token through the refresh token, an exception occurred stating that the refresh token was not valid

FYI, the AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager defaults the ReactiveOAuth2AuthorizationFailureHandler to an instance of RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler, which is why the retry on WebClient works because it authorizes the client once again.

Here is a link to the reference for more details. NOTE: The link is the Servlet reference but it's implemented the same in the WebFlux implementation. We need to update the Reactive docs as it's completely out-of-date.

I'm glad you got it working!

Comment From: alexandre99

@jgrandja thanks for the explanation and the quick return.

Comment From: bhogasena

Hi , i am trying to implement Oauth2 client with password grant type and referring to above code. i am getting below error

Description:

Parameter 1 of method authorizedClientManager in com.dhl.raf.mawm.config.Oauth2ClientConfigLocal required a bean of type 'org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService' that could not be found.

The injection point has the following annotations: - @org.springframework.beans.factory.annotation.Autowired(required=true)

Action:

Consider defining a bean of type 'org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService' in your configuration.