Spring Security OAuth Client for WebClient With Password Grant Type Doesn't Ask For New Token

Viewed 5248

I have a Spring Web application that has a oauth2 resource server configured for it's API endpoints and a completely different oauth2 client for REST calls it makes. The oauth2 client needs to be of a password grant type. Username and password is fixed (not coming from HTTP request). My problem is that after 30 minutes both access token and refresh token expire, so there is no way to do a refresh. I would expect that Spring Security would just ask for new access token, but no. It calls the REST endpoint with the expired one and it returns 403. Here's what I have:

application.yml:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://our.idp.keycloak.host/auth/realms/firstrealm
      client:
        registration:
          my-client-authorization:
            client-id: my_client
            client-secret: ${CLIENT_SECRET}
            authorization-grant-type: password
            scope: openid, profile
        provider:
          my-client-authorization:
            token-uri: https://our.idp.keycloak.host/auth/realms/secondrealm/protocol/openid-connect/token

MyClientConfig.java:

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.security.oauth2.client.OAuth2AuthorizationContext;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;

import java.util.Map;

@Configuration
@RequiredArgsConstructor
public class MyClientConfig {
    @Bean
    WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
        ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
        oauth2Client.setDefaultClientRegistrationId("my-client-authorization");
        return WebClient.builder()
                .apply(oauth2Client.oauth2Configuration())
                .baseUrl("https://the.api.host.to.call")
                .build();
    }

    @Bean
    public OAuth2AuthorizedClientManager authorizedClientManager(
            ClientRegistrationRepository clientRegistrationRepository,
            OAuth2AuthorizedClientRepository authorizedClientRepository
    ) {
        OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
                .password()
                .build();

        DefaultOAuth2AuthorizedClientManager result = new DefaultOAuth2AuthorizedClientManager(
                clientRegistrationRepository,
                authorizedClientRepository
        );

        result.setAuthorizedClientProvider(authorizedClientProvider);
        result.setContextAttributesMapper(oAuth2AuthorizeRequest -> Map.of(
                OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, "user",
                OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, "password"
        ));

        return result;
    }
}

The API call itself:

private <T> T callApi(Function<UriBuilder, URI> uriFunction, Class<T> resultType) {
    return this.webClient
            .get()
            .uri(uriFunction)
            .retrieve()
            .bodyToMono(resultType)
            .block();
}

It works the first time I call it. But after 30 minutes the token is dead and I have no idea how to get a new one. If I switch this to client_credentials grant type, it works, it automatically gets a new token when it is needed. But for some reason I cannot do the same for password grant type.

Edit: So I managed to fix this thanks to this: https://github.com/spring-projects/spring-security/issues/8831 . When I configure the WebClient also for refreshToken, then it crashes when the refresh token expires. But when you do the request again after that, it obtains a new token. So I had to wrap the API call in try catch and if the error is the one that I care about, I call the API again. It's not a very elegant solution, but it works.

2 Answers

It is clearly mentioned here that Spring automatically refreshes the access token when it expires.

I think you need the "reactive" equivalent classes of the ones which you have used. Please try this snippet.

@Configuration
@RequiredArgsConstructor
public class MyClientConfig {
  @Bean
  WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) {
    ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
    oauth2Client.setDefaultClientRegistrationId("my-client-authorization");
    return WebClient.builder()
        .filter(oauth2Client)
        .baseUrl("https://the.api.host.to.call")
        .build();
  }

  @Bean
  public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
      ReactiveClientRegistrationRepository clientRegistrationRepository,
      ReactiveOAuth2AuthorizedClientService authorizedClientService
  ) {
    ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
        .password()
        .build();

    AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager result = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
        clientRegistrationRepository,
        authorizedClientService
    );

    result.setAuthorizedClientProvider(authorizedClientProvider);
    result.setContextAttributesMapper(oAuth2AuthorizeRequest -> Mono.just(Map.of(
        OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, "user",
        OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, "password"
    )));

    return result;
  }
}

Additions to Abhinaba Chakraborty post:

There is no defined "ReactiveOAuth2AuthorizedClientService" and no "clientRegistrationRepository". Based on your Post I have these working settings

@Bean
ReactiveClientRegistrationRepository getRegistrationRepository() {
    ClientRegistration registration = ClientRegistration
            .withRegistrationId(REGISTRATION_ID)
            .tokenUri(TOKEN_URI)
            .clientId(CLIENT_ID)
            .clientSecret(CLIENT_SECRET)
            .authorizationGrantType(new AuthorizationGrantType(GRANT_TYPE))
            .build();
    return new InMemoryReactiveClientRegistrationRepository(registration);
}

@Bean
WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) {
    ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
    oauth2Client.setDefaultClientRegistrationId(REGISTRATION_ID);
    return WebClient.builder()
            .filter(oauth2Client)
            .build();
}

@Bean
public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
        ReactiveClientRegistrationRepository clientRegistrationRepository
) {
    ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
            .password()
            .build();

    AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager clientManager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
            clientRegistrationRepository,
            new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrationRepository)
    );

    clientManager.setAuthorizedClientProvider(authorizedClientProvider);
    clientManager.setContextAttributesMapper(oAuth2AuthorizeRequest -> Mono.just(Map.of(
            OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, USERNAME,
            OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, PASSWORD
    )));

    return clientManager;
}

All constants should be clear. The REGISTRATION_ID is just any value but needs to be equal when creating/using the repository

Related