Describe the bug I am using the AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager with a PasswordReactiveOAuth2AuthorizedClientProvider to authorize requests, but when an authorized client expires it does not fetch a new token because the authorized client has an access token. Since I do not have a RefreshTokenReactiveOAuth2AuthorizedClientProvider I end up with invalid authorized clients. When I do configure a RefreshTokenReactiveOAuth2AuthorizedClientProvider and the refresh token expires, none of the providers will fetch a new token.

There is a specific check in PasswordReactiveOAuth2AuthorizedClientProvider where it depends on the fact that refresh tokens are verified as well: https://github.com/spring-projects/spring-security/blob/fd615535b3662b509dfd9a5b1a125379744a405a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/PasswordReactiveOAuth2AuthorizedClientProvider.java#L96 Compare that with the ClientCredentialsReactiveOAuth2AuthorizedClientProvider: https://github.com/spring-projects/spring-security/blob/fd615535b3662b509dfd9a5b1a125379744a405a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ClientCredentialsReactiveOAuth2AuthorizedClientProvider.java#L72 which does not have a check on refresh token presence.

To Reproduce A configured AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager:

    @Bean
    public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(ReactiveClientRegistrationRepository clientRegistrationRepository,
                                                                         ReactiveOAuth2AuthorizedClientService authorizedClientService,
                                                                         @Value("${oauth2.client.username}") String username,
                                                                         @Value("${oauth2.client.password}") String password) {
        ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
                ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
                        .password()
                        .build();

        AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager =
                new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
                        clientRegistrationRepository, authorizedClientService);
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
        authorizedClientManager.setContextAttributesMapper(contextAttributesMapper(username, password));

        return authorizedClientManager;
    }

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

Expected behavior When only password grant provider is configured (and that should be possible), I expect that even when a refresh token is available, a new token should be fetched based on pasword grant. When password grant provider and refresh token provider is configured, I expect that either one provides a new access token.

Comment From: jgrandja

Thanks for the report @bvklingeren.

When only password grant provider is configured (and that should be possible), I expect that even when a refresh token is available, a new token should be fetched based on pasword grant.

The PasswordReactiveOAuth2AuthorizedClientProvider is intended to perform the password grant, whereas the RefreshTokenReactiveOAuth2AuthorizedClientProvider is intended to perform the refresh_token grant. This is the intended design and we don't want to mix responsibilities by having PasswordReactiveOAuth2AuthorizedClientProvider to also perform the refresh_token grant if a refresh token is available.

Please note, depending on the provider and client configuration, refresh tokens may not be granted to clients performing password grant flows. Providers support either use case but it depends on the configuration. Therefore, based on the configuration only known to the application developer, you would need to configure the ReactiveOAuth2AuthorizedClientProvider to support the grant flows required. To support both grants, you would configure as follows:

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

Furthermore, the password grant may issue refresh tokens (based on provider configuration), however, client_credential grant does not allow this as per spec:

A refresh token SHOULD NOT be included

Regarding...

When password grant provider and refresh token provider is configured, I expect that either one provides a new access token.

The above configuration should work in all cases. Can you provide more detail around your use case and configuration?

If the issue is related to an expired refresh token, then you need to ensure you're using Spring Security 5.3.0 at a minimum, since it introduced ReactiveOAuth2AuthorizationFailureHandler in gh-7699. The default ReactiveOAuth2AuthorizationFailureHandler in AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager will remove the OAuth2AuthorizedClient in the case where the refresh token is expired, and therefore forcing the client to fetch a new access token using the password grant.

Comment From: bvklingeren

Hi Joe,

Thanks for the elaborate answer! I am using Spring Security 5.3.4, so I'm good with that.

What I'm currently experiencing is in our development and acceptance environments (which are not used very much). There is 1 service that acts as a gateway to other services and is replacing tokens of users for one realm with tokens of a fixed user of another realm (as a service account). While the first token is validated for correctness, the other token is fetched based on password and refresh grants. Now, the very first time this service is used, there is no access token present, so it retrieves an access token (and refresh token) and uses that. When the service is not used for quite a time, the access token and the refresh token become invalid. When the service then receives a request, it generates an exception and responds with a statuscode 500. The logs mention the following (truncated) exception:

org.springframework.security.oauth2.client.ClientAuthorizationException: [invalid_grant] Token is not active
    at org.springframework.security.oauth2.client.RefreshTokenReactiveOAuth2AuthorizedClientProvider.lambda$authorize$0(RefreshTokenReactiveOAuth2AuthorizedClientProvider.java:93)

The next request seems to fetch a new token again based on the password grant and is successful again. That's probably due to the new ReactiveOAuth2AuthorizationFailureHandler in AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager.

I'm using the following config:

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

Does the order in which the grants are configured matter maybe? Or do I have to catch the exception and try again?

Comment From: jgrandja

@bvklingeren Yes, the order does matter. Change it to:

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

That way, if the access token is expired during the check in PasswordReactiveOAuth2AuthorizedClientProvider, it will return null and allow the RefreshTokenReactiveOAuth2AuthorizedClientProvider to refresh the expired access token.

However, if the refresh token is also expired/invalid then the default ReactiveOAuth2AuthorizationFailureHandler in AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager will remove the OAuth2AuthorizedClient so when the next request is issued it will go through a new grant flow.

Or do I have to catch the exception and try again?

Yes, catch the exception on an error response and re-issue the request and it will go through a new grant flow.

I'm going to close this issue as the suggestions above should work. If it doesn't, we can reopen and discuss further.

Comment From: bhogasena

Hi Team, I am using PasswordReactiveOAuth2AuthorizedClientProvider and it is not getting new token after expiry and getting 401 each time..how can i get new token

private AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager configureHttpProxy(AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager,
        String username,
        String password) {
    // set the webclient with proxy configuration in the ReactiveOAuth2AccessTokenResponseClient
    WebClientReactivePasswordTokenResponseClient tokenResponseClient = new WebClientReactivePasswordTokenResponseClient();

    tokenResponseClient.setWebClient(
            WebClient.builder()
                    .clientConnector(new ReactorClientHttpConnector(proxyHttpClient()))
                    .build()
    );

    // set the ReactiveOAuth2AccessTokenResponseClient with webclient configuration in the ReactiveOAuth2AuthorizedClientProvider
    PasswordReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = new PasswordReactiveOAuth2AuthorizedClientProvider();
    authorizedClientProvider.setAccessTokenResponseClient(tokenResponseClient);



    // set the ReactiveOAuth2AuthorizedClientProvider in the ReactiveOAuth2AuthorizedClientManager
    authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
    authorizedClientManager.setContextAttributesMapper(contextAttributesMapper(username, password));

    return authorizedClientManager;

Comment From: bhogasena

Below is Complete configuration code

import java.util.HashMap; import java.util.Map; import java.util.function.Function;

import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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.AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.OAuth2AuthorizationContext; import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest; import org.springframework.security.oauth2.client.PasswordReactiveOAuth2AuthorizedClientProvider; 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.endpoint.WebClientReactivePasswordTokenResponseClient; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository; import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; import org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.web.reactive.function.client.ExchangeFilterFunction; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; import reactor.netty.http.client.HttpClient; import reactor.netty.transport.ProxyProvider;

@Configuration

public class Oauth2ClientConfigLocal {

private static final Logger log = LoggerFactory.getLogger(Oauth2ClientConfigLocal.class);

@Bean
ReactiveClientRegistrationRepository getRegistration(
    @Value("${spring.security.oauth2.client.provider.test.token-uri}")
    final String tokenUri,
    @Value("${spring.security.oauth2.client.registration.test.client-id}")
    final String clientId,
    @Value("${spring.security.oauth2.client.registration.test.client-secret}")
    final String clientSecret
) {
    final ClientRegistration registration = ClientRegistration
            .withRegistrationId("test")
            .tokenUri(tokenUri)
            .clientId(clientId)
            .clientSecret(clientSecret)
            .authorizationGrantType(AuthorizationGrantType.PASSWORD)

            .build();
    return new InMemoryReactiveClientRegistrationRepository(registration);
}

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

   return configureHttpProxy(
           new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
                   clientRegistrationRepository,
                   authorizedClientService),
                   username,
                   password

           );

}

@Bean
WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager,
        @Value("${api-host") final String baseURL) {
  ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
  oauth2Client.setDefaultClientRegistrationId("test");

  ReactorClientHttpConnector connector = new ReactorClientHttpConnector(proxyHttpClient());

  return WebClient.builder()
      .filter(oauth2Client)
      .baseUrl(api-host)
      .clientConnector(connector)
      .filter(logRequest())
      .filter(logResponse())
      .build();
}

public HttpClient proxyHttpClient() {

    return HttpClient.create()
           .proxy(proxy -> proxy
                      .type(ProxyProvider.Proxy.HTTP)
                      .host("cloudproxy.dhl.com")
                      .port(10123));
}

private AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager configureHttpProxy(AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager,
        String username,
        String password) {
    // set the webclient with proxy configuration in the ReactiveOAuth2AccessTokenResponseClient
    WebClientReactivePasswordTokenResponseClient tokenResponseClient = new WebClientReactivePasswordTokenResponseClient();

    tokenResponseClient.setWebClient(
            WebClient.builder()
                    .clientConnector(new ReactorClientHttpConnector(proxyHttpClient()))
                    .build()
    );

    // set the ReactiveOAuth2AccessTokenResponseClient with webclient configuration in the ReactiveOAuth2AuthorizedClientProvider
    PasswordReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = new PasswordReactiveOAuth2AuthorizedClientProvider();
    authorizedClientProvider.setAccessTokenResponseClient(tokenResponseClient);



    // set the ReactiveOAuth2AuthorizedClientProvider in the ReactiveOAuth2AuthorizedClientManager
    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);
     };
 }

  private ExchangeFilterFunction logRequest() {
        return (clientRequest, next) -> {
            log.info("Request: {} {}", clientRequest.method(), clientRequest.url());
            clientRequest.headers()
                    .forEach((name, values) -> values.forEach(value -> log.info("{}={}", name, value)));
            return next.exchange(clientRequest);
        };
    }

    private ExchangeFilterFunction logResponse() {
        return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
            log.info("Response: {}", clientResponse.headers().asHttpHeaders().get("property-header"));
            return Mono.just(clientResponse);
        });}

}