In a recent project one of the requirements was to implement an Axis client for a web service with the following additional configuration:
- Web service traffic must go through a proxy
- Basic authentication turned on - login & password required
- Communication is over HTTPS with a self-signed SSL certificate
Although not overly difficult for somebody experienced in creating Apache Axis clients the implementation for the above is not trivial. This is mainly due to SSL’s complexity but also because the configurations to get the above working are required in slightly different places of the Axis client framework.
I hope the below may help someone through some of the steps required.
1. Routing web service calls through a proxy
We need to take care of this one first. If there is a proxy between the client and the server and the client doesn’t know to route the traffic through it the requests won’t even reach the server and will just time out. (If your web browser uses a proxy then your local web service will as well since we are using SOAP over HTTP. You can most likely use the same proxy for the settings for the web service).
Without a proxy configured the connection just times out:
AxisFault
faultCode: {http://schemas.xmlsoap.org/soap/envelope/}Server.userException
faultSubcode:
faultString: java.net.ConnectException: Connection timed out: connect
faultActor:
faultNode:
faultDetail:
{http://xml.apache.org/axis/}stackTrace:java.net.ConnectException: Connection timed out: connect
at java.net.PlainSocketImpl.socketConnect(Native Method)
at java.net.PlainSocketImpl.doConnect(PlainSocketImpl.java:305)
...
Telling Axis to use a proxy is easy. We use Axis properties to set both the proxy host and the port:
private void configureProxy()
{
//this setting (from a config file) controls whether the proxy is to be used or not
if (ApplicationProperties.useProxy())
{
AxisProperties.setProperty("https.proxyHost", "192.168.15.1");
AxisProperties.setProperty("https.proxyPort", "8080");
}
}
2. Basic Authentication
Once Axis knows to use a proxy we can actually reach the server. But calling a web service that requires HTTP basic-auth without the proper credentials will fail. Basic authentication includes a base64 encoded login and password set in the http header of the message. Without these details in the header we will get an error similar to this:
AxisFault
faultCode: {http://xml.apache.org/axis/}HTTP
faultSubcode:
faultString: (401)Authorization Required
faultActor:
faultNode:
faultDetail:
{}:return code: 401
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<HTML><HEAD>
<TITLE>401 Authorization Required</TITLE>
</HEAD><BODY>
<H1>Authorization Required</H1>
This server could not verify that you
are authorized to access the document
requested. Either you supplied the wrong
credentials (e.g., bad password), or your
browser doesn't understand how to supply
the credentials required.<P>
</BODY></HTML>
{http://xml.apache.org/axis/}HttpErrorCode:401
(401)Authorization Required
at org.apache.axis.transport.http.HTTPSender.readFromSocket(HTTPSender.java:744)
at org.apache.axis.transport.http.HTTPSender.invoke(HTTPSender.java:144)
...
Taking care of this is also quite easy as the generated Axis client stub (if you’re using a standard JAX-RPC web service) provides methods to set the user login and password for basic authentication. (Naturally, you need to know what these are to be able to connect)
private void configureBasicAuth() throws ServiceException
{
//the port is the interface for the web service
this.configuredPort = ((MyWebServiceLocator)getService()).getMyWebServicePort();
MyWebServiceBindingStub myStub = (MyWebServiceBindingStub)configuredPort;
myStub.setUsername("userName");
myStub.setPassword("password");
}
3. Communicating over HTTPS with a self-signed SSL certificate
Now that we’re authenticated our web service request makes it through but the SSL connection cannot be established since we’re not providing the correct certificate. In my instance the server requires an unsigned (or self-signed) SSL certificate.
If we don’t provide one the request will bounce with the following error:
AxisFault
faultCode: {http://schemas.xmlsoap.org/soap/envelope/}Server.userException
faultSubcode:
faultString: javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: No trusted certificate found
faultActor:
faultNode:
faultDetail:
{http://xml.apache.org/axis/}stackTrace:javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: No trusted certificate found
at com.sun.net.ssl.internal.ssl.BaseSSLSocketImpl.a(DashoA12275)
at com.sun.net.ssl.internal.ssl.SSLSocketImpl.a(DashoA12275)
at com.sun.net.ssl.internal.ssl.SSLSocketImpl.a(DashoA12275)
at com.sun.net.ssl.internal.ssl.SunJSSE_az.a(DashoA12275)
...
Implementing SSL using certificates is a bit more tricky. It requires creating a custom SSLSocketFactory which uses the correct SSL certificate and setting this factory as the one to be used by Axis. The certificate is packaged into a custom keystore which is packaged and released with the client application. The steps required to implement this are:
- obtain the certificate to be used on the client-side
- create a custom keystore to store the above certificate
- create code for the custom ssl socket factory by extending org.apache.axis.components.net.JSSESocketFactory (reads the custom keystore)
- configure the new custom factory to be used by Axis
To create the custom factory we first need to create a custom keystore which will hold our self-signed certificate. The “Using Self-Signed Certificates for Web Service Security” article explains nicely how this is done.
(The certificate you receive could be a .crt. The keytool utility may throw a tantrum at this as it expects a .cer file. To convert to this Base-64 encoded X.509 certificate, double-click your original .crt file -> Details -> Copy to File and follow the prompts. Save the resulting certificate as a file with a .cer extension and keytool should now be happy)
I used the following command to create my CustomKeystore.jks:
keytool –import –noprompt –trustcacerts –alias CustomKeystoreAlias –file CERTIFICATE.cer –keystore CustomKeystore.jks –storepass customKeystorePassword
(NOTE: for some reason I could not copy the command into the CommandPrompt window and had to type it out)
Once the keystore is created, we include it in the client application - somewhere on the classpath. The next step is to code the custom SSLSocketFactory:
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.util.Hashtable;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import org.apache.axis.components.net.JSSESocketFactory;
import org.apache.axis.components.net.SecureSocketFactory;
import org.apache.commons.lang.StringUtils;
/**
* Custom SSL socket factory to use our integrated keystore.
*
* Based loosely on org.apache.axis.components.net.SunJSSESocketFactory
*/
public class MyCustomSSLSocketFactory extends JSSESocketFactory implements SecureSocketFactory {
/* local keystore password */
private static String MY_KEYSTORE_PASSWORD = "customKeystorePassword";
/* local keystore file (contains the self-signed certificate from the server */
private static String RESOURCE_PATH_TO_KEYSTORE = "CustomKeystore.jks";
/**
* Constructor MyCustomSSLSocketFactory
*
* @param attributes
*/
public MyCustomSSLSocketFactory(Hashtable attributes) {
super(attributes);
}
/**
* Read the keystore, init the SSL socket factory
*
* This overrides the parent class to provide our SocketFactory implementation.
* @throws IOException
*/
protected void initFactory() throws IOException {
try {
SSLContext context = getContext();
sslFactory = context.getSocketFactory();
} catch (Exception e) {
if (e instanceof IOException) {
throw (IOException) e;
}
throw new IOException(e.getMessage());
}
}
/**
* Gets a custom SSL Context.
* This is the main working of this class. The following are the steps that make up our
* custom configuration:
*
* 1. Open our keystore file using the password provided
* 2. Create a KeyManagerFactory and TrustManagerFactory using this file
* 3. Initialise a SSLContext using these factories
*
* @return SSLContext
* @throws WebServiceClientConfigException
* @throws Exception
*/
protected SSLContext getContext() throws WebServiceClientConfigException {
char[] keystorepass = MY_KEYSTORE_PASSWORD.toCharArray();
if (StringUtils.isBlank(new String(keystorepass)))
throw new WebServiceClientConfigException("Could not read password for configured keystore!");
InputStream keystoreFile = this.getClass().getResourceAsStream(RESOURCE_PATH_TO_KEYSTORE);
if (keystoreFile == null)
throw new WebServiceClientConfigException("Could not read the configured keystore file at "+RESOURCE_PATH_TO_KEYSTORE);
try
{
// create required keystores and their corresponding manager objects
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(keystoreFile, keystorepass);
KeyManagerFactory kmf =
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keyStore, keystorepass);
TrustManagerFactory tmf =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keyStore);
// congifure a local SSLContext to use created keystores
SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());
return sslContext;
}
catch (Exception e)
{
throw new WebServiceClientConfigException("Error creating context for SSLSocket!", e);
}
}
}
The last step required is to tell Axis to use the above MyCustomSSLSocketFactory as the default ssl socket factory. This is again achieved by setting AxisProperties:
private void configureSSL()
{
//this is configurable so that it can be switched off in development mode
if (Configuration.useSSL())
{
//use our custom SSLSocketFactory
AxisProperties.setProperty("axis.socketSecureFactory","com.company.application.util.MyCustomSSLSocketFactory");
}
else
{
// The fake factory must not be used in production environment because it ignores any certificates but
// it is convenient to have for testing purposes.
AxisProperties.setProperty("axis.socketSecureFactory","org.apache.axis.components.net.SunFakeTrustSocketFactory");
log.debug("WARNING: SSL CERTIFICATE CONFIGURATION IS TURNED OFF!");
}
}
In the code above we have the option of either using our new fully configured MyCustomSSLSocketFactory or the provided SunFakeTrustSocketFactory. The latter is very useful for testing and will accept any ssl certificate. It is mentioned on the Axis wiki here.
This should be it. Communication should now flow freely between client and server in a reasonably secure manner.
Feel free to post comments.