Thursday, January 13, 2011

Spring WS 2: Client-side WS-Security Using WSS4J

In this tutorial we will implement a client-side WS-Security using WSS4J. We will based our client application on an existing web service client which can be found at Spring WS - MVC: Implementing a Client Tutorial. It's imperative that you've read that first to understand the client application.

Our web service provider will be based from the Spring-WS 2: WS-Security Using WSS4J tutorial. We can also use the web service provider from the Spring-WS 2: WS-Security Using XWSS tutorial. This means you will be required to setup two servers with different ports, i.e two Tomcat instances (which is trivial to setup in Eclipse), one for the client and one for the provier. This also means you've read those articles first to understand what security tokens are required from the client.

We actually just need to edit a single file from our current web service client, the spring-ws.xml config. I'll show you the new config and the old config for comparison purposes.

Here's the new config:

spring-ws.xml (new)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p" 
    xmlns:sws="http://www.springframework.org/schema/web-services"
       xmlns:oxm="http://www.springframework.org/schema/oxm"
          xsi:schemaLocation=
           "http://www.springframework.org/schema/beans 
           http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
            http://www.springframework.org/schema/web-services
           http://www.springframework.org/schema/web-services/web-services-2.0.xsd
           http://www.springframework.org/schema/oxm 
           http://www.springframework.org/schema/oxm/spring-oxm-1.5.xsd">

 <!--
   * The WebServiceTemplate requires a messageSender and messageFactory 
   * In order to facilitate the sending of plain Java objects, the WebServiceTemplate requires a marshaller and unmarshaller.
   * The WebServiceTemplate class uses an URI as the message destination. 
   See: http://static.springsource.org/spring-ws/sites/2.0/reference/html/client.html#client-web-service-template
 -->
 <bean id="webServiceTemplate" class="org.springframework.ws.client.core.WebServiceTemplate"
  p:marshaller-ref="jaxbMarshaller"
  p:unmarshaller-ref="jaxbMarshaller"
  p:defaultUri="http://localhost:8081/spring-ws-wss4j/krams/ws"
  p:messageSender-ref="messageSender">
  <constructor-arg ref="messageFactory"/> 
  <property name="interceptors">
   <list>
    <ref local="wss4jSecurityInterceptor"/>
   </list>
  </property>
 </bean>

 <!-- 
   References
    Chapter 7. Securing your Web services with Spring-WS
    - http://static.springsource.org/spring-ws/sites/2.0/reference/html/security.html
    7.3.  Wss4jSecurityInterceptor
    - http://static.springsource.org/spring-ws/sites/2.0/reference/html/security.html#security-wss4j-security-interceptor 
    Apache WSS4J
    - http://ws.apache.org/wss4j/
    Example of SOAP request authenticated with WS-UsernameToken
    - http://stackoverflow.com/questions/3448498/example-of-soap-request-authenticated-with-ws-usernametoken -->
 <!--  This is not documented in the Spring WS Reference but the API shows that we can add interceptors -->    
 <bean id="wss4jSecurityInterceptor"
  class="org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor">
  <!-- The web service provider requires us to pass a timestamp, 
    username, and password -->
  <property name="securementActions" value="Timestamp UsernameToken" />
  <property name="securementUsername" value="admin" />
  <property name="securementPassword" value="secret" />
  <!-- When the web service replies, it will send a timestamp,
    username, and password as well. We want to verify that it is still
    the same provider -->
  <property name="validationActions" value="Timestamp UsernameToken"/>
  <property name="validationCallbackHandler" ref="callbackHandler" />
 </bean>
 
 <!-- Simple callback handler that validates passwords against a in-memory Properties object. 
  Password validation is done on a case-sensitive basis -->
 <bean id="callbackHandler" class="org.springframework.ws.soap.security.wss4j.callback.SimplePasswordValidationCallbackHandler">
     <property name="users">
         <props>
             <prop key="mojo">mojopass</prop>
             <prop key="user">pass</prop>
         </props>
     </property>
 </bean>
 
 <!-- 
   There are two implementations of the WebServiceMessageSender:
   HttpUrlConnectionMessageSender and CommonsHttpMessageSender.
   
   The CommonsHttpMessageSender provides advanced and easy-to-use functionality 
   (such as authentication, HTTP connection pooling, and so forth).
   This uses the Jakarta Commons HttpClient. 
   See http://static.springsource.org/spring-ws/sites/2.0/reference/html/client.html#client-web-service-template
  -->
 <bean id="messageSender" class="org.springframework.ws.transport.http.CommonsHttpMessageSender"/>
 
 <!-- 
   There are two message factories for SOAP: SaajSoapMessageFactory and AxiomSoapMessageFactory. 
   If no message factory is specified (via the messageFactory property), Spring-WS will use 
   the SaajSoapMessageFactory by default.
   See: http://static.springsource.org/spring-ws/sites/2.0/reference/html/client.html#client-web-service-template
  -->
 <bean id="messageFactory" class="org.springframework.ws.soap.saaj.SaajSoapMessageFactory"/>
  
 <!-- Here we use the Jaxb2 marshaller to marshall and unmarshall our Java objects --> 
 <bean id="jaxbMarshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller"
  p:contextPath="org.krams.tutorial.oxm"/>
  
</beans>

Here's the old config:

spring-ws.xml (old)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p" 
    xmlns:sws="http://www.springframework.org/schema/web-services"
       xmlns:oxm="http://www.springframework.org/schema/oxm"
          xsi:schemaLocation=
           "http://www.springframework.org/schema/beans 
           http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
            http://www.springframework.org/schema/web-services
           http://www.springframework.org/schema/web-services/web-services-2.0.xsd
           http://www.springframework.org/schema/oxm 
           http://www.springframework.org/schema/oxm/spring-oxm-1.5.xsd">

 <!--
   * The WebServiceTemplate requires a messageSender and messageFactory 
   * In order to facilitate the sending of plain Java objects, the WebServiceTemplate requires a marshaller and unmarshaller.
   * The WebServiceTemplate class uses an URI as the message destination. 
   See: http://static.springsource.org/spring-ws/sites/2.0/reference/html/client.html#client-web-service-template
 -->
 <bean id="webServiceTemplate" class="org.springframework.ws.client.core.WebServiceTemplate"
  p:marshaller-ref="jaxbMarshaller"
  p:unmarshaller-ref="jaxbMarshaller"
  p:defaultUri="http://localhost:8081/spring-ws-standalone/krams/ws"
  p:messageSender-ref="messageSender">
  <constructor-arg ref="messageFactory"/> 
 </bean>

 <!-- 
   There are two implementations of the WebServiceMessageSender:
   HttpUrlConnectionMessageSender and CommonsHttpMessageSender.
   
   The CommonsHttpMessageSender provides advanced and easy-to-use functionality 
   (such as authentication, HTTP connection pooling, and so forth).
   This uses the Jakarta Commons HttpClient. 
   See http://static.springsource.org/spring-ws/sites/2.0/reference/html/client.html#client-web-service-template
  -->
 <bean id="messageSender" class="org.springframework.ws.transport.http.CommonsHttpMessageSender"/>
 
 <!-- 
   There are two message factories for SOAP: SaajSoapMessageFactory and AxiomSoapMessageFactory. 
   If no message factory is specified (via the messageFactory property), Spring-WS will use 
   the SaajSoapMessageFactory by default.
   See: http://static.springsource.org/spring-ws/sites/2.0/reference/html/client.html#client-web-service-template
  -->
 <bean id="messageFactory" class="org.springframework.ws.soap.saaj.SaajSoapMessageFactory"/>
  
 <!-- Here we use the Jaxb2 marshaller to marshall and unmarshall our Java objects --> 
 <bean id="jaxbMarshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller"
  p:contextPath="org.krams.tutorial.oxm"/>
  
</beans>

Notice we still have the same basic bean declaration:
webServiceTemplate
messageSender
messageFactory
jaxbMarshaller

However the webServiceTemplate bean now has a reference to the interceptors property:
<property name="interceptors">
   <list>
    <ref local="wss4jSecurityInterceptor"/>
   </list>
  </property>
This is the property that's responsible for all the client-side WS-Security using WSS4J. It's really a simple addition to our existing web service client.

We also have to modify the defaultUri to match our current web service provider:
http://localhost:8081/spring-ws-wss4j/krams/ws
You may need to modify this property depending on how you setup your provider.

The Client-side WSS4J WS-Security
Let's examine further the wss4jSecurityInterceptor bean. Notice it contains a couple of properties. If you have read the Spring-WS 2: WS-Security Using WSS4J, you will find the following settings familiar because we're using the same exact interceptor!

<bean id="wss4jSecurityInterceptor"
  class="org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor">
  <!-- The web service provider requires us to pass a timestamp, 
    username, and password -->
  <property name="securementActions" value="Timestamp UsernameToken" />
  <property name="securementUsername" value="admin" />
  <property name="securementPassword" value="secret" />
  <!-- When the web service replies, it will send a timestamp,
    username, and password as well. We want to verify that it is still
    the same provider -->
  <property name="validationActions" value="Timestamp UsernameToken"/>
  <property name="validationCallbackHandler" ref="callbackHandler" />
 </bean>
 
 <!-- Simple callback handler that validates passwords against a in-memory Properties object. 
  Password validation is done on a case-sensitive basis -->
 <bean id="callbackHandler" class="org.springframework.ws.soap.security.wss4j.callback.SimplePasswordValidationCallbackHandler">
     <property name="users">
         <props>
             <prop key="mojo">mojopass</prop>
             <prop key="user">pass</prop>
         </props>
     </property>
 </bean>
The configuration defines policies pertaining to client-side WS-Security. It contains two parts:

1. The elements that the client must provide when sending a request message.
  • securementActions: When the client sends a message, a Timestamp and UsernameToken elements (including the username and password) will be added in the SOAP Header section.
  • securementUsername: The username that the provider expects
  • securementPassword: The password that the provider expects

2. The elements that the provider must provide when sending a response message.
  • validationActions: When the provider sends a reply, it should contain the actions we specified in the validationActions. If it doesn't contain the actions, then the reply is invalid. For our current configuration, we expect a Timestamp and UsernameToken as well from the provider. This is like a dual verification on both sides. The provider requires a Timestamp and UsernameToken. The client requires the same
  • validationCallbackHandler: When the provider sends a reply, it adds a username and password attributes. To validate these values in the client-side, we provide a callback handler, a SimplePasswordValidationCallbackHandler (see below) . In our current configuration, the client expects that the provider will send either of the following username/password pair:
    mojo/mojopass
    user/pass
<bean id="wss4jSecurityInterceptor"
 <!-- Simple callback handler that validates passwords against a in-memory Properties object. 
  Password validation is done on a case-sensitive basis -->
 <bean id="callbackHandler" class="org.springframework.ws.soap.security.wss4j.callback.SimplePasswordValidationCallbackHandler">
     <property name="users">
         <props>
             <prop key="mojo">mojopass</prop>
             <prop key="user">pass</prop>
         </props>
     </property>
 </bean>

Run the Client Application
Let's run our client application and examine the actual SOAP request message sent by the client:

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
 <SOAP-ENV:Header>
  <wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" SOAP-ENV:mustUnderstand="1">
   <wsse:UsernameToken xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="UsernameToken-2" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
    <wsse:Username>admin</wsse:Username>
    <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">1pN5RLpgXRhZxlYwHovlstf75do=</wsse:Password>
    <wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">TOvp53qb3mPvX4YMi+Ac8g==</wsse:Nonce>
    <wsu:Created>2011-01-13T14:17:59.486Z</wsu:Created>
   </wsse:UsernameToken>
   <wsu:Timestamp xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="Timestamp-1">
    <wsu:Created>2011-01-13T14:17:59.473Z</wsu:Created>
    <wsu:Expires>2011-01-13T14:22:59.473Z</wsu:Expires>
   </wsu:Timestamp>
  </wsse:Security>
 </SOAP-ENV:Header>
 <SOAP-ENV:Body>
  <ns2:subscriptionRequest xmlns:ns2="http://krams915.blogspot.com/ws/schema/oss">
   <ns2:id>1234567</ns2:id>
   <ns2:name>John Smith</ns2:name>
   <ns2:email>john@dummy.com</ns2:email>
  </ns2:subscriptionRequest>
 </SOAP-ENV:Body>
</SOAP-ENV:Envelope>
Notice the SOAP Header has been augmented with a Timestamp and UsernameToken including the username and password: admin/(encrypted password). A Nonce and Created date has been added as well.

Run the Provider Application
Let's run our web service provider and examine the actual SOAP reply message sent by the provider:

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
 <SOAP-ENV:Header>
  <wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" SOAP-ENV:mustUnderstand="1">
   <wsse:UsernameToken xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="UsernameToken-3" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
    <wsse:Username>mojo</wsse:Username>
    <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">mojopass</wsse:Password>
    <wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">wVExJqRYGv9tYq8YUGkFyw==</wsse:Nonce>
    <wsu:Created>2011-01-13T14:24:17.026Z</wsu:Created>
   </wsse:UsernameToken>
   <wsu:Timestamp xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="Timestamp-2">
    <wsu:Created>2011-01-13T14:24:17.018Z</wsu:Created>
    <wsu:Expires>2011-01-13T14:29:17.018Z</wsu:Expires>
   </wsu:Timestamp><wsse11:SignatureConfirmation xmlns:wsse11="http://docs.oasis-open.org/wss/oasis-wss-wssecurity-secext-1.1.xsd" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="SigConf-1"/>
  </wsse:Security>
 </SOAP-ENV:Header>
 <SOAP-ENV:Body>
  <subscriptionResponse xmlns="http://krams915.blogspot.com/ws/schema/oss">
   <code>SUCCESS</code>
   <description>User has been subscribed</description>
  </subscriptionResponse>
 </SOAP-ENV:Body>
</SOAP-ENV:Envelope>"

Notice the SOAP Header has been augmented with a Timestamp and UsernameToken including the username and password: mojo/mojopass. A Nonce and Created date has been added as well. These properties are exactly the same in both the provider and the client.

However, do not be mistaken. Our client application can also access XWSS-based web service providers! Just make sure you've set the correct URI. If you like to test this, feel free to run the provider application from Spring-WS 2: WS-Security Using XWSS tutorial.

That's it. We've completed our client-side WS-Security application using WSS4J. It's amazing how we enabled security on the client side. We just added a simple Wss4jSecurityInterceptor (the same bean we used in the provider application) and everything is running smoothly.

To run the client application, use the following URL:
http://localhost:8080/spring-ws-client-wss4j/krams/main/subscribe

Here's a screenshot of the client application:

The best way to learn further is to try the actual application

Download the project
You can access the project site at Google's Project Hosting at http://code.google.com/p/spring-ws-2-0-0-rc2-tutorial/

You can download the project as a Maven build. Look for the spring-ws-client-wss4j.zip in the Download sections.

You can run the project directly using an embedded server via Maven.
For Tomcat: mvn tomcat:run
For Jetty: mvn jetty:run

If you want to learn more about Spring MVC and integration with other technologies, feel free to read my other tutorials in the Tutorials section.
StumpleUpon DiggIt! Del.icio.us Blinklist Yahoo Furl Technorati Simpy Spurl Reddit Google I'm reading: Spring WS 2: Client-side WS-Security Using WSS4J ~ Twitter FaceBook

Subscribe by reader Subscribe by email Share

8 comments:

  1. Really nice tutorial, truly showcasing how easy we have it now that wss4jSecurityInterceptor is here :)

    ReplyDelete
  2. Hi Mark,
    Thank you for the nice tutorial. Do you have also some example how to set up wss4j 1.6+ validators?
    I do not want to use callbackHandlers but validators that doesn't need call the method setPassword to the callback.

    Thank you!

    ReplyDelete
  3. I get error:

    PropertyAccessException 1: org.springframework.beans.MethodInvocationException: Property 'securementActions' threw exception; nested exception is java.lang.NoSuchMethodError: org.apache.ws.security.util.WSSecurityUtil.decodeAction(Ljava/lang/String;Ljava/util/List;)I

    Maybe I use wrong version? Why it does not find method?

    ReplyDelete
    Replies
    1. Ey! Did you solve that problem!? We are having the same issue!

      Delete
  4. This comment has been removed by the author.

    ReplyDelete
  5. Any idea how I could use the interceptor to allow for different usernames and passwords accessing the backend Soap API? My application needs to access this Soap API on behalf of the users of my application and they have their own credentials in the Soap API. So I can't just set the username and password once on configuration, I need to have it look up the username and password when it goes to make a soap call. Can you point me in the write direction?

    ReplyDelete
  6. It's very straightforward to find out any matter on net as compared to books, as I found this post at this web page.

    ReplyDelete