Monday, March 5, 2007

Securing Spring-WS Client with XWSS

Warning: this post is now obsolete. Spring-WS WS-Security now work on the client side as well.

I was really excited about the new release of Spring Web Services (M3) and its new client side API. I like the simplicity and the elegance of the template based approach which is already used in various parts of Spring. In this post, I will show you how I used XWSS to add WS-Security support to web services invocations.
I used XWSS 2.0 from jwsdp-2.0. I had to add xmlsec.jar from the jwsdp-shared/lib in order for the example to work.

Server Side

As a simple example, I will configure the web service in the Echo sample to require a wss user name token.
To add WS-Security support, we need to add a XwsSecurityInterceptor to spring-ws-servlet.xml. The call back handler is a SimplePasswordValidationCallbackHandler that expects a user named "cafe" and "babe" as a password.

<bean id="wsSecurityInterceptor" class="org.springframework.ws.soap.security.xwss.XwsSecurityInterceptor">
    <property name="policyConfiguration" value="classpath:wss-server-config.xml" />
    <property name="callbackHandlers">
        <list>
            <bean id="passwordValidationHandler" class="org.springframework.ws.soap.security.xwss.callback.SimplePasswordValidationCallbackHandler">        
                <property name="users">
                    <props>
                        <prop key="cafe">babe</prop>
                    </props>
                </property>
            </bean>
        </list>
    </property>
</bean>
wsSecurityInterceptor has to be added to the list of interceptors of payloadMapping

<property name="interceptors">
    <list>
        <ref local="loggingInterceptor" />
        <ref local="wsSecurityInterceptor" />
        <ref local="validatingInterceptor" />
    </list>
</property>
Finally, the server side XWSS configuration is read from a file named wss-server-config.xml from the classpath. With this configuration, XWSS will expect a WS-Security header with a user name token
<xwss:SecurityConfiguration xmlns:xwss="http://java.sun.com/xml/ns/xwss/config">
    <xwss:RequireUsernameToken passwordDigestRequired="false" nonceRequired="false"/>
</xwss:SecurityConfiguration>

Client Side

Adding WS-Security support to the client side is quite simple. Spring-WS client provides WebServiceMessageCallback in order to operate on the message before it is sent. We will use it to replace the message with a secured one. But first we have to configure XWSS on the client side. In the XML configuration file, we will instruct XWSS to include a user name token in the SOAP header.

<xwss:SecurityConfiguration dumpMessages="true" xmlns:xwss="http://java.sun.com/xml/ns/xwss/config">
    <xwss:UsernameToken name="cafe" password="babe" useNonce="false" digestPassword="false"/>
</xwss:SecurityConfiguration>
The dumpMessages=true will cause the secured message to be printed in the console which is useful for debugging. A full description of the available configuration options can be found here.
In EchoClient.java we need to use a com.sun.xml.wss.XWSSProcessor and a com.sun.xml.wss.ProcessingContext to process the message.

public class EchoClient extends WebServiceGatewaySupport {
....
  private static XWSSProcessor cprocessor;
  private static ProcessingContext context;
....
  public static void main(String[] args) throws IOException,
      XWSSecurityException {
....
    Resource xwssConfig = new ClassPathResource("wss-client-config.xml");
    if (!xwssConfig.exists()) {
      throw new FileNotFoundException();
    }

    XWSSProcessorFactory factory = XWSSProcessorFactory.newInstance();
    cprocessor = factory.createProcessorForSecurityConfiguration(xwssConfig
                             .getInputStream(), null);
    context = new ProcessingContext();
The interesting part is in the echo() method. We will use the sendAndReceive that accepts a WebServiceMessageCallback. Please note that XWSS only works with SAAJ so SaajSoapMessageFactory have to be used.

getWebServiceTemplate().sendAndReceive(requestSource,
  new WebServiceMessageCallback() {
    public void doInMessage(WebServiceMessage message)
      throws IOException {
      // We can cast safley to SaajSoapMessage
      SaajSoapMessage saajSoapMessage = (SaajSoapMessage) message;
      SOAPMessage saajMessage = saajSoapMessage.getSaajMessage();
      try {
        context.setSOAPMessage(saajMessage);
        SOAPMessage securedMessage = cprocessor
                  .secureOutboundMessage(context);
        saajSoapMessage.setSaajMessage(securedMessage);
      }
      catch (XWSSecurityException e) {
        throw new XwsSecuritySecurementException(e.getMessage());
      }
  }
}, result);
If every thing works ok, you should see this on your console (name spaces omitted for brevity):
INFO: ==== Sending Message Start ====
<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header>
<wsse:Security xmlns:wsse="..." SOAP-ENV:mustUnderstand="1">
<wsse:UsernameToken xmlns:wsu="..." wsu:Id="XWSSGID-1172860158358-935964365">
<wsse:Username>cafe</wsse:Username>
<wsse:Password Type="...#PasswordText">****</wsse:Password>
</wsse:UsernameToken>
</wsse:Security>
</SOAP-ENV:Header>
<SOAP-ENV:Body>
<echoRequest xmlns="...">Hello</echoRequest>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
==== Sending Message End  ====