javaspring-bootspring-wsspring-oauth2

How to use spring.security.oauth2.client with SOAP calls, initially sent by org.springframework.ws.client.core.WebServiceTemplate?


We have a Spring Boot microservice that does the SOAP call to the external system using org.springframework.ws.client.core.WebServiceTemplate.

Now the system would be protected with Keycloak, so all the request need to beak the auth token.

If it was a REST API, I would just replace the pre-existed RestTemplate with OAuth2RestTemplate. But how to instrument the calls initially done by the org.springframework.ws.client.core.WebServiceTemplate ?

So, I understand, I should put the authentication header manually with value 'Bearer ....token there...'. How I can retrieve that part manually to put it into the request?


Solution

  • The problem was caused by

    1. Existing library code, based on org.springframework.ws.client.core.WebServiceTemplate, so large and huge for rewriting it using WebClient, compatible with OAuth2 SpringSecurity or use depricated OAuth2RestTemplate

    2. The webservice we previously communicated with, turns into protected with Gravitee and accepts queries with JWT tokens only. So, the only change here is to add the Authentication header with 'Bearer ....token there...'

    3. We initiate the call from the scheduled jo in the microservice. So, it should be getting token from the Keycloak before the request and be able to update it with time. No one does the explicit authorization like in the frontend, so the OAuth2 client should use client-id and client-secret to connect with no human involved

    The Solution

    1. At the beginning, we define the Interceptor to the SOAP calls, that will pass the token as a header, via a Supplier function taking it wherever it can be taken:

       public class JwtClientInterceptor implements ClientInterceptor {
      
       private final Supplier<String> jwtToken;
      
       public JwtClientInterceptor(Supplier<String> jwtToken) {
           this.jwtToken = jwtToken;
       }
      
       @Override
       public boolean handleRequest(MessageContext messageContext) {
           SoapMessage soapMessage = (SoapMessage) messageContext. getRequest();
           SoapHeader soapHeader = soapMessage.getSoapHeader();
           soapHeader.addHeaderElement(new QName("authorization"))
                   .setText(String. format("Bearer %s", jwtToken.get()));
           return true;
       }
      
       @Override
       public boolean handleResponse(MessageContext messageContext) throws WebServiceClientException {
           return true;
       }
      
       @Override
       public boolean handleFault(MessageContext messageContext) throws WebServiceClientException {
           return true;
       }
      
       @Override
       public void afterCompletion(MessageContext messageContext, Exception ex) throws WebServiceClientException {
      
       }
      

      }

    2. Then pass it to the template in addition to other pre-existed interceptor to be called in config class:

       protected WebServiceTemplate     buildWebServiceTemplate(Jaxb2Marshaller marshaller,
                                                                HttpComponentsMessageSender messageSender, String uri,     Supplier<String> jwtToken) {
           WebServiceTemplate template = new WebServiceTemplate();
           template.setMarshaller(marshaller);
           template.setUnmarshaller(marshaller);
           template.setMessageSender(messageSender);
           template.setDefaultUri(uri);
           ClientInterceptor[] clientInterceptors =     ArrayUtils.addAll(template.getInterceptors(), new Logger(), new     JwtClientInterceptor(jwtToken));
      
           template.setInterceptors(clientInterceptors);
           return template;
       }
      
    3. Then add the Spring Security Oath2 Client library

       compile 'org.springframework.security:spring-security-oauth2-client:5.2.1.RELEASE'
      
    4. We create OAuth2AuthorizedClientService bean, that uses a standard ClientRegistrationRepository (the repository is initiated through usage of @EnableWebSecurity annotation on the @Configuration class, but please double check about that)

       @Bean
       public OAuth2AuthorizedClientService     oAuth2AuthorizedClientService(ClientRegistrationRepository     clientRegistrationRepository) {
               return  new     InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository);
          }
      
    5. Then create a OAuth2AuthorizedClientManager

           @Bean
           public OAuth2AuthorizedClientManager authorizedClientManager(
                   ClientRegistrationRepository clientRegistrationRepository,
                   OAuth2AuthorizedClientRepository authorizedClientRepository) {
      
               Authentication authentication = new Authentication()     {
                   @Override
                   public Collection<? extends GrantedAuthority> getAuthorities() {
                       GrantedAuthority grantedAuthority = new GrantedAuthority() {
                           @Override
                           public String getAuthority() {
                               return "take_a_needed_value_from_property";
                           }
                       };
                       return Arrays.asList(grantedAuthority);
                   }
      
                   @Override
                   public Object getCredentials() {
                       return null;
                   }
      
                   @Override
                   public Object getDetails() {
                       return null;
                   }
      
                   @Override
                   public Object getPrincipal() {
                       return new Principal() {
                           @Override
                           public String getName() {
                               return "our_client_id_from_properties";
                       }
                   };
               }
      
               @Override
               public boolean isAuthenticated() {
                   return true;
               }
      
               @Override
               public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
      
               }
      
               @Override
               public String getName() {
                   return "take_a_needed_name_from_properties";
               }
           };
       //we need to emulate Principal there, as other classes relies on it. In fact, Principal isn't needed for the app which is a client and just do the call, as nothing is authorized in the app against this Principal itself
      
               OAuth2AuthorizationContext oAuth2AuthorizationContext     =     OAuth2AuthorizationContext.withClientRegistration(clientRegistrationRepository.findByRegistrationId("keycloak")).
                       principal(authentication).
                       build();
                   oAuth2AuthorizationContext.getPrincipal().setAuthenticated(true);
               oAuth2AuthorizationContext.getAuthorizedClient();
      
               OAuth2AuthorizedClientProvider authorizedClientProvider =     OAuth2AuthorizedClientProviderBuilder.builder().
               //refreshToken().
               clientCredentials(). //- we use this one according to our set up
               //authorizationCode().
                       build();
      
               OAuth2AuthorizedClientService oAuth2AuthorizedClientService =     oAuth2AuthorizedClientService(clientRegistrationRepository); //use the bean from before step here
      
               AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager =
                       new AuthorizedClientServiceOAuth2AuthorizedClientManager(
                               clientRegistrationRepository,     oAuth2AuthorizedClientService);
               OAuth2AuthorizedClient oAuth2AuthorizedClient =     authorizedClientProvider.authorize(oAuth2AuthorizationContext);
                   authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
             oAuth2AuthorizedClientService.saveAuthorizedClient(oAuth2AuthorizedClient, 
         oAuth2AuthorizationContext.getPrincipal());
         //this step is needed, as without explicit authorize call, the 
         //oAuth2AuthorizedClient isn't initialized in the service
      
               return authorizedClientManager;
           }
      
    6. Provide a method for supplied function that can be called each time to retrieve the JWT token from the security stuff (repository and manager). Here it should be auto-updated, so we just call for retrieving it

        public Supplier<String> getJwtToken() {
               return () -> {
                   OAuth2AuthorizedClient authorizedClient = authorizedClientService.loadAuthorizedClient("keycloak", "we_havePout_realm_there_from_the_properties");
                   return     authorizedClient.getAccessToken().getTokenValue();
               };
           }
      
    7. Pass this Consumer to the @Bean, defining the WebServiceTemplate's

        @Bean
           public Client client(@Qualifier("Sender1") HttpComponentsMessageSender bnfoMessageSender,
                                @Qualifier("Sender2")     HttpComponentsMessageSender uhMessageSender) {
               WebServiceTemplate sender1= buildWebServiceTemplate(buildSender1Marshaller(),     sender1MessageSender, properties.getUriSender1(),getJwtToken());
               WebServiceTemplate sender2 = buildWebServiceTemplate(buildSender2Marshaller(), sender2MessageSender, properties.getUriSender2(),getJwtToken());
               return buildClient(buildRetryTemplate(), sender1, sender2);
           }
      
    8. We add Spring Security Client values to application.yaml in order to configure it.

       spring:
        security:
           oauth2:
             client:
               provider:
                 keycloak:
                   issuer-uri: https://host/keycloak/auth/realms/ourrealm
               registration:
                 keycloak:
                   client-id: client_id
                   client-secret: client-secret-here
                   authorization-grant-type: client_credentials     //need to add explicitly, otherwise would try other grant-type by default and never get the token!
                   client-authentication-method: post //need to have this explicitly, otherwise use basic that doesn't fit best the keycloak set up
                   scope: openid //if your don't have it, it checks all available scopes on url like https://host/keycloak/auth/realms/ourrealm/ .well-known/openid-configuration keycloak and then sends them as value of parameter named 'scope' in the query for retrieving the token; that works wrong on our keycloak, so to replace this auto-picked value, we place the explicit scopes list here