Apache Web Services Security Using WSS4J

This document presents a practical approach of how to use WSS4J to add simple security headers to both client and web service implemented with Axis 1.*

Adding security at the UsernameToken level for a web service means adding a specific request header with the user name and password, with the password either in clear or encrypted (digest)

WSS4J is implemented to permit adding security headers without changing the actual web service code.

Libraries

The current implementation uses the following libraries:

  • Axis web service implementation version 1.4 download
  • WSS4J implementation from Apache download
  • XML security implementation, also from Apache download

Client

The client uses the axis libraries, the client stub classes generated for the specific service and the libraries for WSS4J and XML security plus dependencies.

The WSS4J handler must be registered to the client handler chain in order to manipulate the SOAP request and get the security headers in place. The handler receives the user name (principal) directly from the client code through properties, and the password through a callback class.

The client stub classes are generated with WSDL2Java class from the service wsdl file.

This is the code for the client / without security added

  1.  
  2. package service.test.client;
  3.  
  4. import java.net.URL;
  5.  
  6. import javax.xml.rpc.holders.StringHolder;
  7.  
  8. import com.services.www.data.Item;
  9. import com.services.www.data.holders.ParametersHolder;
  10. import com.services.www.services.InfoServiceLocator;
  11. import com.services.www.services.InfoServicePortType;
  12.  
  13. public class TestClient extends BaseClient {
  14. public static void main(String[] args){
  15. try {
  16. new TestClient().start();
  17. }
  18. catch(Exception e){
  19. e.printStackTrace();
  20. }
  21. }
  22.  
  23. private void start() throws Exception {
  24. // get the locator
  25. InfoServiceLocator locator = new InfoServiceLocator();
  26.  
  27. URL address = new URL("http://localhost:8000/service/services/InfoService");
  28. InfoServicePortType service = locator.getInfoService(address);
  29.  
  30. // get the service
  31. StringHolder statusCode = new StringHolder();
  32. StringHolder comments = new StringHolder();
  33. ParametersHolder parameters = new ParametersHolder();
  34.  
  35. // call the operation
  36. service.getInformation("name here", "test id",
  37. new Item[]{
  38. new Item("id1", "name1", "comments1"),
  39. new Item("id2", "name2", "comments2")
  40. },
  41. statusCode, comments, parameters);
  42.  
  43. // show the results (a collection of helper methods
  44. // a
  45. display("status", statusCode.value);
  46. display("comments", comments.value);
  47.  
  48. display(parameters);
  49. }
  50. }
  51.  

The request as generated with this client looks like this

  1.  
  2. <?xml version="1.0" encoding="UTF-8"?>
  3. <soapenv:Envelope
  4. xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
  5. xmlns:xsd="http://www.w3.org/2001/XMLSchema"
  6. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  7. <soapenv:Body>
  8. <getInformation xmlns="http://www.services.com/data">
  9. <name xmlns="">name here</name>
  10. <id xmlns="">test id</id>
  11. <items xmlns="">
  12. <item>
  13. <itemId>id1</itemId>
  14. <itemName>name1</itemName>
  15. <comments>comments1</comments>
  16. </item>
  17. <item>
  18. <itemId>id2</itemId>
  19. <itemName>name2</itemName>
  20. <comments>comments2</comments>
  21. </item>
  22. </items>
  23. </getInformation>
  24. </soapenv:Body>
  25. </soapenv:Envelope>
  26.  
  27.  

Now the only thing to do is to add the security. As mentioned, adding security headers is accomplished without changing the actual service code. The process involves registering and configuring a handler and providing a callback class implementation.

There are two ways of registering the handler, as a part of the client configuration file (.wsdd) or programatically. Most of the time the application already has a configuration file, there should not be any reason to maintain another one, therefore the only approach presented here is the programmatical one.

Here is the client code that includes the security. Please note the following: when adding the security handler programmatically, the Axis framework expects a class that implements javax.xml.rpc.handler.Handler. When the handler is defined through the wsdd configuration file, then a class implementing org.apache.axis.Handler is expected. If this is not followed, the error triggered is something stating that "the handler cannot be created", however this is a wrap around a ClassCastException

WSS4J implementation provides two classes implementing org.apache.axis.Handler, WSDoAllReceiver and WSDoAllSender, and one implementing javax.xml.rpc.handler.Handler which is WSS4JHandler

Below is the code, please note the method addSecurityHeaders. The handler chain object is retrieved from the locator object and a new handler is added

  1.  
  2. package service.test.client;
  3.  
  4. import java.net.URL;
  5. import java.util.List;
  6. import java.util.TreeMap;
  7.  
  8. import javax.xml.namespace.QName;
  9. import javax.xml.rpc.handler.HandlerInfo;
  10. import javax.xml.rpc.handler.HandlerRegistry;
  11. import javax.xml.rpc.holders.StringHolder;
  12.  
  13. import org.apache.ws.security.handler.WSS4JHandler;
  14.  
  15. import com.services.www.data.Item;
  16. import com.services.www.data.holders.ParametersHolder;
  17. import com.services.www.services.InfoServiceLocator;
  18. import com.services.www.services.InfoServicePortType;
  19.  
  20. public class TestSecureClient extends BaseClient {
  21. public static void main(String[] args){
  22. try {
  23. new TestSecureClient().start();
  24. }
  25. catch(Exception e){
  26. e.printStackTrace();
  27. }
  28. }
  29.  
  30. private void start() throws Exception {
  31. // get the locator
  32. InfoServiceLocator locator = new InfoServiceLocator();
  33.  
  34. URL address = new URL("http://localhost:8000/service/services/InfoService");
  35. InfoServicePortType service = locator.getInfoService(address);
  36.  
  37. // register the security handler and provide the attributes
  38.  
  39. // get the service
  40. StringHolder statusCode = new StringHolder();
  41. StringHolder comments = new StringHolder();
  42. ParametersHolder parameters = new ParametersHolder();
  43.  
  44. // add security headers
  45. addSecurityHeaders(locator);
  46.  
  47. // call the operation
  48. service.getInformation("name here", "test id",
  49. new Item[]{
  50. new Item("id1", "name1", "comments1"),
  51. new Item("id2", "name2", "comments2")
  52. },
  53. statusCode, comments, parameters);
  54.  
  55. // show the results (a collection of helper methods
  56. // a
  57. display("status", statusCode.value);
  58. display("comments", comments.value);
  59.  
  60. display(parameters);
  61. }
  62.  
  63. private void addSecurityHeaders(InfoServiceLocator locator) {
  64. HandlerRegistry registry = locator.getHandlerRegistry();
  65. QName portName = new QName("http://www.services.com/services", "InfoService");
  66.  
  67. List chain = registry.getHandlerChain(portName);
  68.  
  69. // create the new handler info for the WSS4J handler
  70. HandlerInfo info = new HandlerInfo();
  71. info.setHandlerClass(WSS4JHandler.class);
  72.  
  73. TreeMap tmap = new TreeMap();
  74.  
  75. tmap.put("send.action", "UsernameToken");
  76. tmap.put("receive.action", "NoSecurity");
  77.  
  78. tmap.put("passwordCallbackClass", "service.test.client.SecureCallback");
  79. tmap.put("deployment", "client");
  80.  
  81. // tmap.put("passwordType", "PasswordText");
  82. tmap.put("passwordType", "PasswordDigest");
  83.  
  84. // tmap.put("flow", "request");
  85. tmap.put("user", "testuser");
  86.  
  87. info.setHandlerConfig(tmap);
  88.  
  89. chain.add(info);
  90. }
  91. }
  92.  
  93.  

A couple of things about parameters.

passwordType has the value PasswordDigest. This will encrypt the password sent in the request as it is visible below. If PasswordText was used instead, the security layer would have passed the password "in clear".

send.action and receive.action - they confirm the type of security header request and in the response respectively. When one is present, the other is also mandatory. In our case, the request (send.action) will contain UsernameToken security header, the response will not contain anything. As receive.action is mandatory, will be added with the NoSecurity value. The two parameters can be replaced with action when both request and response must be treated the same way. In the current case this is not possible as the response will not contain any security header

What happens if action was used with UsernameToken: the handler would create a Security element in the header and expect one in the response. As the response does not contain any security header, then the client would trigger the error with the message WSS4JHandler: Request does not contain required Security header That is a lot misleading as the security header is missing from the response, not from the request.

After adding this functionality, the request will contain the security header as per standard, with the password encrypted.

  1.  
  2. <?xml version="1.0" encoding="UTF-8"?>
  3. <soapenv:Envelope
  4. xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
  5. xmlns:xsd="http://www.w3.org/2001/XMLSchema"
  6. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  7. <soapenv:Header>
  8. <wsse:Security
  9. xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
  10. soapenv:mustUnderstand="1">
  11. <wsse:UsernameToken
  12. xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
  13. wsu:Id="UsernameToken-1">
  14. <wsse:Username>testuser</wsse:Username>
  15. <wsse:Password
  16. Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">SwZD8+A6Jk2stfubiJR5d0MO+mU=
  17. </wsse:Password>
  18. <wsse:Nonce
  19. EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">bMN8g5OcJgz9QbCSw8ix5g==
  20. </wsse:Nonce>
  21. <wsu:Created>2010-09-13T16:57:27.876Z
  22. </wsu:Created>
  23. </wsse:UsernameToken>
  24. </wsse:Security>
  25. </soapenv:Header>
  26. <soapenv:Body>
  27. <getInformation xmlns="http://www.services.com/data">
  28. <name xmlns="">name here</name>
  29. <id xmlns="">test id</id>
  30. <items xmlns="">
  31. <item>
  32. <itemId>id1</itemId>
  33. <itemName>name1</itemName>
  34. <comments>comments1</comments>
  35. </item>
  36. <item>
  37. <itemId>id2</itemId>
  38. <itemName>name2</itemName>
  39. <comments>comments2</comments>
  40. </item>
  41. </items>
  42. </getInformation>
  43. </soapenv:Body>
  44. </soapenv:Envelope>
  45.  
  46.  

The callback defined under the property passwordCallbackClass is a Java class that implements javax.security.auth.callback.CallbackHandler. The only purpose of this class is to provide the password associated with a principal. How the callback is implemented provides full flexibility.

Following is the code associated with this callback. getPassword is a custom method implemented by the programmer that actually returns the password given the principal. As this is specific to each situation, we left it out of the document.

  1.  
  2. public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
  3. // given the user, provide the password that will be sent in the
  4. // security header (clear format or digest)
  5. // we assume the provided callback array is not null
  6. for(int count = 0; count < callbacks.length; count ++){
  7. Callback crt = callbacks[count];
  8.  
  9. /* our callback must be of type WSPasswordCallback for the UsernameToken */
  10. if(crt instanceof WSPasswordCallback){
  11. WSPasswordCallback pc = (WSPasswordCallback) crt;
  12.  
  13. String user = pc.getIdentifier();
  14.  
  15. // implement getPassword to actually provide
  16. // the correct password for the specified principal
  17. pc.setPassword(getPassword(user));
  18. }
  19. }
  20. }
  21.  

Server

For the server side the security handler is configured in the server-config.wsdd file, the web service configuration file.

Below is the transport tag, after configuring the handler. Note the fact the handler must implement org.apache.axis.Handler and not javax.xml.rpc.handler.Handler

  1.  
  2. <transport name="http">
  3. <requestFlow>
  4. <handler type="URLMapper"/>
  5. <handler type="java:org.apache.axis.handlers.http.HTTPAuthHandler"/>
  6. <handler type="java:org.apache.ws.axis.security.WSDoAllReceiver">
  7. <parameter name="action" value="UsernameToken" />
  8. <parameter name="passwordType" value="PasswordDigest" />
  9. <parameter name="passwordCallbackClass" value="com.services.www.callback.SecureCallback" />
  10. </handler>
  11. </requestFlow>
  12. </transport>
  13.  

In the current example WSDoAllReceiver is used. The parameters define the type of security header, including the way the password must be processed (PasswordDigest).

The callback class defined is identical with the one used on the client.

The action is defined under the property action. It is applied to the request only anyway, as the configuration is part of the <requestFlow>. That is the reason it is declared different than on the client side, when we needed to specifically define the action for request and response.

This concludes the current example. Please ensure the latest version of xml security is used