I recently had to put in place a public facing soap web service with Spring and WS-Security. While the tutorials on Spring Source are great, if you requirements deviate from the sample tutorials it is difficult to track down any extra information needed.

First off, my requirements were:

  • WS-Security with PKI.
  • Must use the Wss4jSecurityInterceptor interceptor.
  • The body of the soap message was to be signed and encrypted, and some parts of the header must also be signed.
  • Timestamps must be included in the soap header.
  • The body was encrypted so WS-Addressing was required, and I used the ws-addressing action to decide the message/function to call.

There were many clients of the service, each with their own keys, so I needed to use the ws-addressing From address to choose the encryption key for the return message.

A full project isn’t included here, just excerpts from the code where it deviated from the tutorial.

I needed to use Action Endpoint mapping instead of payload mapping.

@Action(value=REQUEST_ACTION, output=RESPONSE_ACTION)
@ResponsePayload
public Element handleRequest(@RequestPayload Element)
{

REQUEST_ACTION and RESPONSE_ACTION match the request and response action from your WSDL.

The next problem I hit was the interceptor chain, shown below, didn’t appear to get invoked when the action endpoint mapping when configured in spring-ws-servlet.xml.


<sws:interceptors>
     <bean class="org.springframework.ws.soap.server.endpoint.interceptor.SoapEnvelopeLoggingInterceptor" />
</sws:interceptors>

So, the interceptor chain had to be configured in code, instead of xml.

Notice that I used setSecurementParts() to specify the parts of the message I wanted signed.


@Configuration
public class ActionInterceptorImpl implements InitializingBean {

 /**
  * App Context for loading resources
  */
 @Autowired
 ApplicationContext appContext = null;

 // Pick up the AnnotationActionEndpointMapping instance, created by
 // 
 // do not throw an exception, if isn´t there, required = false
 private @Autowired(required = false)
 AnnotationActionEndpointMapping annotationActionEndpointMapping = null;


 public void afterPropertiesSet() throws Exception {

  if (annotationActionEndpointMapping != null) {

        // Allocate an array for our interceptors
   EndpointInterceptor[] postInterceptors = new EndpointInterceptor[ ];
   EndpointInterceptor[] preInterceptors = new EndpointInterceptor[1];

   postInterceptors[0] = loggingInterceptor();
   postInterceptors[1] = validatingInterceptor();

        // Security needs be a pre Interceptor
   preInterceptors[0] = securityInterceptor();

   annotationActionEndpointMapping.setPreInterceptors(preInterceptors);
   annotationActionEndpointMapping.setPostInterceptors(postInterceptors);

  }
 }

 @Bean
 SoapEnvelopeLoggingInterceptor loggingInterceptor() {
  SoapEnvelopeLoggingInterceptor i = new SoapEnvelopeLoggingInterceptor();

  return i;
 }

 @Bean
 PayloadValidatingInterceptor validatingInterceptor() {

             // Create the intercepter
  PayloadValidatingInterceptor i = new PayloadValidatingInterceptor();

  // Don't validate the Response, if there's an error the Security Inteceptor won't invoke.
  i.setValidateResponse(false);  
  i.setValidateRequest(true);

  // Get your list of xsd schemas from wherever you store them  
  .
  .
  .

  i.setSchemas( _SCHEMAS_ );

  return i;
 }

 @Bean
 Wss4jSecurityInterceptor securityInterceptor() {
  Wss4jSecurityInterceptor i = new Wss4jSecurityInterceptor();

  i.setValidateRequest(true);

  try {

   CryptoFactoryBean cryptoFactoryBean = new CryptoFactoryBean();
   cryptoFactoryBean
     .setKeyStoreLocation(new FileSystemResource( _KETSTORE_ ));

   cryptoFactoryBean.setKeyStorePassword( _KEYSTORE_PASSWORD_ );
   cryptoFactoryBean.afterPropertiesSet();
   Crypto crypto = cryptoFactoryBean.getObject();

   i.setValidationActions("Timestamp Signature Encrypt");
   i.setValidationDecryptionCrypto(crypto);

   KeyStoreCallbackHandler callbackHandler = new KeyStoreCallbackHandler();
   callbackHandler.setPrivateKeyPassword( _PRIVATE_KEY_PASSWORD_ );

   i.setValidationCallbackHandler(callbackHandler);
   i.setValidationSignatureCrypto(crypto);

   i.setSecurementActions("Timestamp Signature Encrypt");
   i.setSecurementUsername( _USERNAME_ );
   i.setSecurementPassword( _PASSWORD_ );

   i.setSecurementEncryptionKeyIdentifier( _ALIAS_OF_PRIVATE_KEY_ );

   i.setSecurementEncryptionCrypto(crypto);
   i.setSecurementSignatureCrypto(crypto);

      // Parts of the response to sign
      // .. in this case the body and the ws-addressing To field.
   i.setSecurementSignatureParts("{Element}{http://schemas.xmlsoap.org/soap/envelope/}Body;{Element}{http://www.w3.org/2005/08/addressing}To");

  } catch (Exception e) {
   logger.fatal("Failed to enable security", e);

   return null;
  }
  return i;
 }
}

Next, the response needs to be encrypted using the public key of the requester. In the tutorials this is done by Wss4jSecurityInterceptor.securementEncryptionUser(), but this sets the encryption user globally for the interceptor, so all requests will be encrypted with the key of the user set here, and since I wouldn’t know the requester until run time, I needed to call this for every request.

To do this, I extended Wss4jSecurityInterceptor and overrode the secureMessage() method, this method is called everytime the message needs to be secured.


public class Wss4jSecurityInterceptorWithNClients extends
 Wss4jSecurityInterceptor {

 protected void secureMessage(SoapMessage soapMessage,
   MessageContext messageContext) throws WsSecuritySecurementException {

  SoapMessage request = (SoapMessage) messageContext.getRequest();  
  SoapHeader header = request.getSoapHeader();


  // Determine the key alias of the key of the from user.
  // How you do this is dependant on your implementation
  // I got the WS-A From address and did a lookup from a properties file.
  .
  .
  .

  setSecurementEncryptionUser( _FROM_ALIAS_ );

  try
  {
   super.secureMessage(soapMessage, messageContext);
  }
  catch(Exception e)
  {
   .
   .
   .
  }

  // Reset it, in case the next request doesn't specify a from address.
  setSecurementEncryptionUser("");
 }
}

It’s important to make sure that the scope of Wss4jSecurityInterceptorWithNClients is set to Request, because this method is not thread safe.

Next, I changed ActionInterceptorImpl to use Wss4jSecurityInterceptorWithNClients instead of Wss4jSecurityInterceptor.

There were two other issues I noticed: The Interceptor will verify all signatures in the request, but I couldn’t find a way for it to ensure/enforce particular fields were signed. In the end I checked that the fields which required signatures has them by examining their attributes. If the WS-A ReplyTo field was set to something invalid in the request, it caused an exception which broke the interceptor chain and resulted in unsecured responses being set. As a quick work around I removed any ReplyTo entry as I didn’t require it.