Search code examples
springweb-servicesspring-bootcxf

How to add SAML token to CXF client request in Spring Boot


We're building a CXF client in Spring Boot. The SAML token to authenticate/authorize against the SOAP server is provided to our app in custom HTTP header from an external auth proxy with every request. Hence, I need a way to add the provided token to every outgoing CXF request.

I know that I could possibly register a custom CXF out interceptor for that. However,

  • How would I go about registering that interceptor in Spring Boot?
  • If not done with an interceptor what would be the alternatives?

Currently, the Spring config looks like this:

@Configuration
public class MyConfig {

  @Bean
  public PartnerServicePortType partnerServicePortType() {
    PartnerServicePortType partnerServicePortType = new PartnerServiceV0().getPartnerService();

(PartnerServiceV0 is generated from the service's WSDL with Maven.)

In the above config class we don't currently declare/configure a CXF bus bean.


Solution

  • One possible solution is this:

    @Configuration
    public class MyConfig {
    
      @Bean
      public PartnerServicePortType partnerServicePortType() {
        PartnerServicePortType service = new PartnerServiceV0().getPartnerService();
        configure(service, path, baseUrl);
        return service;
      }
    
      private void configureService(BindingProvider bindingProvider, String path, String baseUrl) {
        // maybe try the approach outlined at https://github
        // .com/kprasad99/kp-soap-ws-client/blob/master/src/main/java/com/kp/swasthik/soap/CxfConfig.java#L24
        // as an alternative
        bindingProvider.getRequestContext().put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, baseUrl + path);
    
        Endpoint cxfEndpoint = ClientProxy.getClient(bindingProvider).getEndpoint();
        cxfEndpoint.getInInterceptors().add(cxfLoggingInInterceptor);
        cxfEndpoint.getInFaultInterceptors().add(cxfLoggingInInterceptor);
        cxfEndpoint.getOutInterceptors().add(addSamlAssertionInterceptor);
      }
    }
    

    And the interceptor

    import lombok.SneakyThrows;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.lang3.StringUtils;
    import org.apache.cxf.binding.soap.SoapHeader;
    import org.apache.cxf.binding.soap.SoapMessage;
    import org.apache.cxf.binding.soap.interceptor.AbstractSoapInterceptor;
    import org.apache.cxf.interceptor.Fault;
    import org.apache.cxf.phase.Phase;
    import org.opensaml.core.xml.XMLObject;
    import org.opensaml.core.xml.XMLObjectBuilder;
    import org.opensaml.core.xml.XMLObjectBuilderFactory;
    import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
    import org.opensaml.core.xml.io.Marshaller;
    import org.opensaml.core.xml.io.MarshallingException;
    import org.opensaml.saml.saml2.core.Assertion;
    import org.opensaml.soap.wssecurity.Created;
    import org.opensaml.soap.wssecurity.Expires;
    import org.opensaml.soap.wssecurity.Security;
    import org.opensaml.soap.wssecurity.Timestamp;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    import org.w3c.dom.Element;
    
    import javax.xml.namespace.QName;
    import java.time.ZoneOffset;
    import java.time.ZonedDateTime;
    import java.time.format.DateTimeFormatter;
    
    
    /**
     * Adding SOAP header with SAML assertion to request.
     */
    @Slf4j
    @Component
    public class AddSamlAssertionInterceptor extends AbstractSoapInterceptor {
    
      private final SamlAssertionExtractor samlAssertionExtractor;
    
      @Autowired
      public AddSamlAssertionInterceptor(SamlAssertionExtractor samlAssertionExtractor) {
        super(Phase.POST_LOGICAL);
        this.samlAssertionExtractor = samlAssertionExtractor;
      }
    
      @Override
      public void handleMessage(SoapMessage message) throws Fault {
        String decodedToken = SamlTokenHolder.getDecodedToken();
        if (StringUtils.isBlank(decodedToken)) {
          log.trace("Not adding SOAP header with SAML assertion because SAML token is blank.");
        } else {
          log.trace("Got decoded SAML token: {}", decodedToken);
          log.trace("Adding SOAP header with SAML assertion to request.");
          SoapHeader header = createSoapHeaderWithSamlAssertionFrom(decodedToken);
          message.getHeaders().add(header);
        }
      }
    
      private SoapHeader createSoapHeaderWithSamlAssertionFrom(String decodedToken) {
        Assertion assertion = samlAssertionExtractor.extractAssertion(decodedToken);
    
        Security security = createNewSecurityObject();
        security.getUnknownXMLObjects().add(createTimestampElementFrom(assertion));
        security.getUnknownXMLObjects().add(assertion);
    
        log.trace("Creating new SOAP header with WS-Security element for '{}'.",
          assertion.getSubject().getNameID().getValue());
        SoapHeader header = new SoapHeader(security.getElementQName(), marshallToDom(security));
        header.setMustUnderstand(config.isMustUnderstandHeader());
        return header;
      }
    
      @SneakyThrows(MarshallingException.class)
      private Element marshallToDom(Security security) {
        Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(security);
        return marshaller.marshall(security);
      }
    
      /*
       * SAML requirements documented at https://docs.oasis-open.org/wss/v1.1/wss-v1.1-spec-errata-os-SOAPMessageSecurity
       * .htm#_Toc118717167. Both timestamps must be in UTC and formatted to comply with xsd:dateTime.
       */
      private Timestamp createTimestampElementFrom(Assertion assertion) {
        Timestamp timestamp = (Timestamp) createOpenSamlXmlObject(Timestamp.ELEMENT_NAME);
        Created created = (Created) createOpenSamlXmlObject(Created.ELEMENT_NAME);
        Expires expires = (Expires) createOpenSamlXmlObject(Expires.ELEMENT_NAME);
        // alternative would be to use timestamp from assertion like so assertion.getConditions().getNotBefore()
        created.setValue(ZonedDateTime.now(ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT));
        // security semantics should ensure that the expiry date here is the same as the expiry of the SAML assertion
        expires.setValue(assertion.getConditions().getNotOnOrAfter().toString());
        timestamp.setCreated(created);
        timestamp.setExpires(expires);
        return timestamp;
      }
    
      private Security createNewSecurityObject() {
        return (Security) createOpenSamlXmlObject(Security.ELEMENT_NAME);
      }
    
      private XMLObject createOpenSamlXmlObject(QName elementName) {
        XMLObjectBuilderFactory builderFactory = XMLObjectProviderRegistrySupport.getBuilderFactory();
        XMLObjectBuilder<Security> builder = (XMLObjectBuilder<Security>) builderFactory.getBuilder(elementName);
        return builder.buildObject(elementName);
      }
    }