Adding authentication to an Apache CXF Web Service Client

In my last post, I provided steps for creating an Apache CXF Web Service Client. Now that client only works with Web Services that do not require you to authenticate. In this post, I will provide an example of how add authentication.

Step 1. Create a CXF Web Service client

Step 2. Create a CallbackHandler. The code below is actually from a previous post pertaining to authenticating an Axis 1.4 Web Service Client:

import java.io.IOException;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.ws.security.WSPasswordCallback;
public class SamplePWCallback implements CallbackHandler {
   private Configuration _config;
   private static Log _logger = LogFactory.getLog(SamplePWCallback.class);
   public SamplePWCallback () {
      super();
      _logger.info("Initializing Sample PW Callback");
      _config = null;
      try {
         _config = new PropertiesConfiguration("sample.properties");
      }
      catch (ConfigurationException ce) {
         _logger.error("Error loading configuration file", ce);
      }
   }
   public void handle(Callback[] callbacks) throws IOException,
      UnsupportedCallbackException {
      for (int i = 0; i < callbacks.length; i++) {
         if (callbacks[i] instanceof WSPasswordCallback) {
            WSPasswordCallback pc = (WSPasswordCallback)callbacks[i];
            // set the password given a username
            if (_config.getString("username").equals(pc.getIdentifier())) {
               pc.setPassword(_config.getString("password"));
            }
         } 
         else {
            throw new UnsupportedCallbackException(callbacks[i], "Unrecognized Callback");
         }
      }
   }
}

Step 3. Update your client to use your CallbackHandler. For the purposes of demonstration, let’s assume the global weather Web Service that I used in the previous blog post required authentication. The client would have to be updated to look similar to the sample one below:

import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import net.webservicex.*;
import org.apache.cxf.binding.soap.saaj.SAAJOutInterceptor;
import org.apache.cxf.endpoint.Client; 
import org.apache.cxf.endpoint.Endpoint;                      
import org.apache.cxf.frontend.ClientProxy;
import org.apache.cxf.interceptor.LoggingInInterceptor;
import org.apache.cxf.interceptor.LoggingOutInterceptor;
import org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor; 
import org.apache.ws.security.handler.WSHandlerConstants;
import org.apache.ws.security.WSConstants;
public class HelloCXF {
   public static void main (String args[]) {
      try {
         GlobalWeather service = new GlobalWeather(new URL("file:globalweather.wsdl"));
         GlobalWeatherSoap weatherSoap = service.getGlobalWeatherSoap();
         Client client = ClientProxy.getClient( weatherSoap );
         Endpoint endPoint = client.getEndpoint();   
         Map<String, Object> outProperties = new HashMap<String, Object>();
         outProperties.put( WSHandlerConstants.ACTION,
               WSHandlerConstants.USERNAME_TOKEN );
         outProperties.put( WSHandlerConstants.USER, _config.getString("username"));
         outProperties.put( WSHandlerConstants.PASSWORD_TYPE, WSConstants.PW_TEXT );
         outProperties.put( WSHandlerConstants.PW_CALLBACK_CLASS,
               SamplePWCallback.class.getName() );
         WSS4JOutInterceptor wssOut = new WSS4JOutInterceptor( outProperties );
         endPoint.getOutInterceptors().add( wssOut );
         endPoint.getOutInterceptors().add( new SAAJOutInterceptor() );
         endPoint.getInInterceptors().add( new LoggingInInterceptor() );
         endPoint.getOutInterceptors().add( new LoggingOutInterceptor() );

         System.out.println("Weather for New York");
         System.out.println(weatherSoap.getWeather("New York", "United States"));
      }
      catch (MalformedURLException mue) {
         System.err.println("problem with the wsdl url");
         System.exit(1);
      }
   }
}

Creating an Apache CXF Web Service Client

So in early blog posts, I discussed how to make an Axis 1.4 client and secure it. Axis 1.4, however, has been end of lifed; no more work is being done on it. Now, if you are thinking about using Axis 2 to make a client in order to stick with a package that is actively being maintained, I would strongly advise against it. Axis 2 is really designed to be used on a Java application server and it is not friendly at all for standalone clients. There are literally 2 dozen jars plus modules that you need to add to your classpath if you want your client to work.

Fortunately, there is another Apache Web Service project, CXF. From what I can see, CXF is really a continuation of where Axis 1.4 left off. Even for Java Web apps, CXF is much friendlier to use than Axis 2. 

Below is an example of how to setup a CXF client

Step 1. Download CXF

Step 2. Download the wsdl that you want to use. For this post, similar to the previous ones, I am going to use the global weather wsdl

Step 3. Use the wsdl2java CXF utility to generate Java code representing the global weather Web Service. The command should look like the following:

wsdl2java globalweather.wsdl

Step 4. Copy the code from step 3 into your source code directory. If you are working in Eclipse, you can drag and drop the code into your src directory

Step 5. Add the following files from the CXF lib directory to your classpath. If you are working in Eclipse, you can drag and drop these files into your own lib directory

  • commons-collections-3.2.1.jar
  • commons-lang-2.4.jar
  • commons-logging-1.1.1.jar
  • cxf-2.2.12.jar
  • geronimo-activation_1.1_spec-1.0.2.jar
  • geronimo-annotation_1.0_spec-1.1.1.jar
  • geronimo-javamail_1.4_spec-1.6.jar
  • geronimo-jaxws_2.1_spec-1.0.jar
  • geronimo-servlet_2.5_spec-1.2.jar
  • geronimo-stax-api_1.0_spec-1.0.1.jar
  • geronimo-ws-metadata_2.0_spec-1.1.2.jar
  • jaxb-api-2.1.jar
  • jaxb-impl-2.1.13.jar
  • neethi-2.0.4.jar
  • saaj-api-1.3.jar
  • saaj-impl-1.3.2.jar
  • wsdl4j-1.6.2.jar
  • wss4j-1.5.10.jar
  • wstx-asl-3.2.9.jar
  • xml-resolver-1.2.jar
  • XmlSchema-1.4.7.jar
  • xmlsec-1.4.3.jar

Step 6. Create a Web Service client. Below is an example of a client that works with the global weather Web Service:

import java.net.MalformedURLException;
import java.net.URL;
import net.webservicex.*;
public class HelloCXF {
   public static void main (String args[]) {
      try {
         GlobalWeather service = new GlobalWeather(new URL("globalweather.wsdl"));
         GlobalWeatherSoap weatherSoap = service.getGlobalWeatherSoap();
         System.out.println("Weather for New York");
         System.out.println(weatherSoap.getWeather("New York", "United States"));
      }
      catch (MalformedURLException mue) {
         System.err.println("problem with the wsdl url");
         System.exit(1);
      }
   }
}

Axis 1.4 Web Service Client Side authentication

So in a previous post, I gave an example of how to setup an Axis 1.4 Web Service client. That is fine if you do not have to login to use the service. Often times however, you do have to authenticate your client especially if your work has paid for the third party Web Service that you want to use. Here you want to use WSS4J. Below are the steps to get yourself setup:

Step 1. Download WSS4J

Step 2. Add the contents of the WSS4J lib directory to your classpath. If you are working with a project in Eclipse, you probably want to copy the contents of WSS4J lib to your own lib directory

Step 3. Create a deployment descriptor file. Below is an example of one that we will refer to as sample.wsdd:

<deployment xmlns="http://xml.apache.org/axis/wsdd/" xmlns:java="http://xml.apache.org/axis/wsdd/providers/java">
 <transport name="http" pivot="java:org.apache.axis.transport.http.HTTPSender"/>
  <globalConfiguration >
   <requestFlow >
    <handler type="java:org.apache.ws.axis.security.WSDoAllSender" >
     <parameter name="action" value="UsernameToken"/>
     <parameter name="user" value="sampleUsername"/>
     <parameter name="passwordCallbackClass" value="SamplePWCallback"/>
     <parameter name="passwordType" value="PasswordDigest"/>
    </handler>
   </requestFlow >
  </globalConfiguration >
</deployment>

Step 4. Create a CallbackHandler. In my example below, I use the Apache configuration package to get the username and password from a properties file in order to avoid hardcoding it.

import java.io.IOException;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.ws.security.WSPasswordCallback;
public class SamplePWCallback implements CallbackHandler {
   private Configuration _config;
   private static Log _logger = LogFactory.getLog(SamplePWCallback.class);
   public SamplePWCallback () {
      super();
      _logger.info("Initializing Sample PW Callback");
      _config = null;
      try {
         _config = new PropertiesConfiguration("sample.properties");
      }
      catch (ConfigurationException ce) {
         _logger.error("Error loading configuration file", ce);
      }
   }
   public void handle(Callback[] callbacks) throws IOException,
      UnsupportedCallbackException {
      for (int i = 0; i < callbacks.length; i++) {
         if (callbacks[i] instanceof WSPasswordCallback) {
            WSPasswordCallback pc = (WSPasswordCallback)callbacks[i];
            // set the password given a username
            if (_config.getString("username").equals(pc.getIdentifier())) {
               pc.setPassword(_config.getString("password"));
            }
         } 
         else {
            throw new UnsupportedCallbackException(callbacks[i], "Unrecognized Callback");
         }
      }
   }
}

Step 5. Access your wsdd in your Web Service client. Let’s assume the global weather Web Service that I used in the previous Axis 1.4 blog post required authentication. Here is what your client would look like.

import NET.webserviceX.www.*;
import org.apache.axis.configuration.FileProvider;
public class HelloWeather {
   public static void main(String[] args) throws Exception {
      FileProvider wsdd = new FileProvider("sample.wsdd");
      wsdd.setSearchClasspath(true);
      GlobalWeatherLocator locator = new GlobalWeatherLocator(wsdd);
      GlobalWeatherSoap globalWeatherSoap = locator.getGlobalWeatherSoap();
      System.out.println("Weather for New York");
      System.out.println(globalWeatherSoap.getWeather("New York", "United States"));
   }
}

How to build an Axis 1.4 XML-RPC based Web Service client

So Web Services have been around for a while now and one would think that the popular packages out there would support most any service. Well unfortunately, that is a bad assumption to make. That is because of how long Web Services have existed. Some earlier versions employed a variation of the SOAP message that we use today called XML-RPC. Very few contemporary Web Service packages support XML-RPC yet even now, you will run into someone using it, like I did at work.

I found that Axis 1.4 works pretty much out of the box for these older style Web Services. Axis2, like the rest though, does not support XML-RPC. 

To create a client, you will be best served by using the WSDL2Java utility. To use it, you will need to make sure the following jars are in your classpath; they come packaged in the Axis 1.4 download so you don’t have to google for them.

  • axis.jar
  • jaxrpc.jar
  • saaj.jar
  • commons-logging-1.0.4.jar
  • commons-discovery-0.2.jar
  • wsdl4j-1.5.1.jar

Once you have added those jars to your classpath, you can let Axis do most of the work for you in creating a client. For this example, we are going to build a client that gets the weather from the global weather Web Service. This Web Service is pretty basic so it is good for learning how to make a client however, its uptime can be spotty at times.

Here are the steps for creating a clients.

Step 1: Download the globalweather.wsdl. This file contains information describing the Web Service.

Step 2: Run the following command
java org.apache.axis.wsdl.WSDL2Java globalweather.wsdl

This Axis utility will generate a Java package from the wsdl necessary for interacting with the Web Service. This code will be layout out in the directory where you ran the utility and at the time of writing this article should have to following structure
NET/webserviceX/www/

Step 3: Copy the code you generated in Step 2 to your client’s working directory. You might not need to do this step if you are going to write your client in the same directory in which you generated the package. If you are using Eclipse or any IDE though, you will want to copy the NET directory into one of your source code folders. I use Eclipse so I literally dragged and dropped it.

Step 4: Create a client class. The code below is a simple client that gets the weather for New York.

import NET.webserviceX.www.*;
public class HelloWeather {
    public static void main(String[] args) throws Exception {
        GlobalWeatherLocator locator = new GlobalWeatherLocator();
        GlobalWeatherSoap globalWeatherSoap = locator.getGlobalWeatherSoap();
        System.out.println("Weather for New York");
        System.out.println(globalWeatherSoap.getWeather("New York", "United States"));
    }
}

Axis hides a lot of the boiler plate work involved in a Web Service and it is pretty simple to use. The only complexity that you will encounter is when the Web Services you use get complex and generally those ones have some support documentation that explains what all the end points are and how you should use them.