Search code examples
springspring-securityoauth-2.0spring-security-oauth2

Spring WebClient and shared client credential token for all requests


I tried to use Spring WebClient and Spring Security OAuth2 Client for communication between two applications. I need to use OAuth2 Client Credentials grant type and use the same credentials and the same token for all requests between these applications. In the OAuth2 terminology resource owner is an application A itself and resource server is an application B (it is Keycloak Admin API). I use Spring Boot auto configuration from application.yaml and servlet environment. So I implemented it like this:

WebClient buildWebClient(ClientRegistrationRepository clientRegistrationRepository,
    OAuth2AuthorizedClientRepository authorizedClientRepository, String clientName) {
  final ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = new ServletOAuth2AuthorizedClientExchangeFilterFunction(
      clientRegistrationRepository, authorizedClientRepository);
  oauth2Client.setDefaultClientRegistrationId(clientName);

  return WebClient.builder()
      .apply(oauth2Client.oauth2Configuration())
      .build();
}

I found that I have an issue and invalid tokens (I use Keycloak and if my Keycloak is restarted then all old tokens are invalid) are never removed (you can see it in issue #9477) so I started debuging whole process of authorization in ServletOAuth2AuthorizedClientExchangeFilterFunction. I found that tokens are saved per user of the application A. It means that every user of the application A has its own token for authorization to the application B. But this is not my intended behavior I want to use only one token in the application A for consuming a resource from the application B.

I think that I found two solutions but both of them are not optimal.

Solution 1.: Add another default request after ServletOAuth2AuthorizedClientExchangeFilterFunction which replaces user Authentication in attributes by an application Authentication. Problem is that it dependes on an internal implemention of ServletOAuth2AuthorizedClientExchangeFilterFunction which use a private attribute AUTHENTICATION_ATTR_NAME = Authentication.class.getName();.

  return WebClient.builder()
      .apply(oauth2Client.oauth2Configuration())
      .defaultRequest(spec -> spec.attributes(attr -> 
           attr.put(AUTHENTICATION_ATTR_NAME, new CustomApplicaitonAuthentication())
        )
      .build();

Solution 2.: Use a custom implementation of OAuth2AuthorizedClientRepository instead of default AuthenticatedPrincipalOAuth2AuthorizedClientRepository (which I have to do anyway because of the issue with removing invalid tokens) where I ignore user's Authentication and I use a custom application Authentication.

@Override
public <T extends OAuth2AuthorizedClient> T loadAuthorizedClient(String clientRegistrationId, Authentication principal,
                                                                        HttpServletRequest request) {
  return this.authorizedClientService.loadAuthorizedClient(clientRegistrationId, new CustomApplicaitonAuthentication());
}

@Override
public void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal,
                                    HttpServletRequest request, HttpServletResponse response) {
  this.authorizedClientService.saveAuthorizedClient(authorizedClient, new CustomApplicaitonAuthentication());
}

But I think that both my solutions are not optimal. Is there another way how to do it more straightforward?


Solution

  • I found solution because I needed to run it without a servlet request (messaging, scheduler etc.). Therefore I needed to update it and use the AuthorizedClientServiceOAuth2AuthorizedClientManager instead of the default DefaultOAuth2AuthorizedClientManager. Then I reallized that it does the same thing like I need.

     @Bean
     WebClient buildWebClient(
          OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager,
          OAuth2AuthorizationFailureHandler oAuth2AuthorizationFailureHandler,
          String clientName) {
    
        final ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = new ServletOAuth2AuthorizedClientExchangeFilterFunction(
            oAuth2AuthorizedClientManager);
        oauth2Client.setDefaultClientRegistrationId(clientName);
        oauth2Client.setAuthorizationFailureHandler(oAuth2AuthorizationFailureHandler);
    
        return WebClient.builder()
            .apply(oauth2Client.oauth2Configuration())
            .build();
      }
    
    
      @Bean
      public OAuth2AuthorizedClientManager authorizedClientManager
          (ClientRegistrationRepository clients, OAuth2AuthorizedClientService service, OAuth2AuthorizationFailureHandler authorizationFailureHandler) {
        AuthorizedClientServiceOAuth2AuthorizedClientManager manager =
            new AuthorizedClientServiceOAuth2AuthorizedClientManager(clients, service);
        manager.setAuthorizationFailureHandler(authorizationFailureHandler);
        return manager;
      }
    
      @Bean
      public OAuth2AuthorizationFailureHandler resourceServerAuthorizationFailureHandler(
          OAuth2AuthorizedClientService authorizedClientService) {
        return new RemoveAuthorizedClientOAuth2AuthorizationFailureHandler(
            (clientRegistrationId, principal, attributes) -> 
              authorizedClientService.removeAuthorizedClient(clientRegistrationId, principal.getName()));
      }
    
    

    We had to add a custom OAuth2AuthorizationFailureHandler because if you do not use this constructor ServletOAuth2AuthorizedClientExchangeFilterFunction(ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientRepository authorizedClientRepository) and you use a custom OAuth2AuthorizedClientManager then it is not configured and you have to do it manually.