Tuesday, March 08, 2011

NTLM from an Axis (SOAP) service client - in 3 steps

Keywords:
NTLM authentication Negotiate Apache axis SOAP IIS Windows Integrated Authentication CommonsHTTPSender NTCredentials

Problem:
Authenticating a service request with BASIC authentication is (relatively) straightforward:

import java.net.URL;
import org.apache.axis.client.Stub;
import com.example.service.Example;
import com.example.service.ExampleServiceLocator;
import com.example.service.ExampleRequest;
import com.example.service.ExampleResponse;

// get access to the web service
ExampleServiceLocator locator = new ExampleServiceLocator();
String serviceURL = "http://server/application/services/example";
Example example = locator.getexample(new URL(serviceURL));
// set credentials
((Stub)example).setUsername("myusername");
((Stub)example).setPassword("mypassword");


// setup request
ExampleRequest request = new ExampleRequest();
request.setProperty("SomeProperty");

ExampleResponse response = example.example(request);


What if the (SOAP) service being called required NTLM authentication (e.g. the service is running in IIS and security is set as "Windows Integrated Authentication")?

Solution:
The following three steps are assuming Axis 1.x. The Apache Axis Client Tips and Tricks is a good reference, in particular for step 2, but also for other "tips".

Step 1: Add Apache commons-httpclient (3.1) and commons-codec libraries


Note you must add the commons httpclient jar file and not the (latest/refactored) apache httpclient to the project - or you will get ClassNotFound exceptions.

Step 2: Define custom client-config with CommonsHTTPSender


It's mentioned in the "Tips and Tricks" article mentioned above, but you can either: (a) define a custom client-config.wsdd file in the classpath before axis.jar; (b) edit the generated ...ServiceLocator.java generated class and make it override getEngine...; or (c) at runtime simply feed the customised config XML to your ...ServiceLocator object.

I prefer the latter - for example, define a static method with the config XML as a string:

protected static org.apache.axis.EngineConfiguration getEngineConfiguration() {
java.lang.StringBuffer sb = new java.lang.StringBuffer();
sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n");
sb.append("<deployment name=\"defaultClientConfig\"\r\n");
sb.append("xmlns=\"http://xml.apache.org/axis/wsdd/\"\r\n");
sb.append("xmlns:java=\"http://xml.apache.org/axis/wsdd/providers/java\">\r\n");
// sb.append("<transport name=\"http\" pivot=\"java:org.apache.axis.transport.http.HTTPSender\" />\r\n");
sb.append("<transport name=\"http\" pivot=\"java:org.apache.axis.transport.http.CommonsHTTPSender\" />\r\n");
sb.append("<transport name=\"local\" pivot=\"java:org.apache.axis.transport.local.LocalSender\" />\r\n");
sb.append("<transport name=\"java\" pivot=\"java:org.apache.axis.transport.java.JavaSender\" />\r\n");
sb.append("</deployment>\r\n");
org.apache.axis.configuration.XMLStringProvider config =
new org.apache.axis.configuration.XMLStringProvider(sb.toString());
return config;
}


Then the call to the locator would become:

// get access to the web service
ExampleServiceLocator locator = new ExampleServiceLocator(getEngineConfiguration());


Step 3: Set the username as DOMAIN\username


Set the username as you did with BASIC authentication but you must ensure is set in the form DOMAIN\username (keeping in mind that if expressing this in java code - as a string - or as a property value in a properties file this would be set as "DOMAIN\\username" - \\ being the escape sequence for \):

((Stub)example).setUsername("MY_NT_DOMAIN\\myusername");
((Stub)example).setPassword("mypassword");


With the above 3 steps covered you're using NTLM.

Notes:
Avoid setting the system property -Djava.ext.dirs as the above relies on the sunjce_provider.jar library which is in JRE_HOME\lib\ext by default. Ext-path problems may give you errors such as:
"Cannot find any provider supporting DES/ECB/NoPadding"


Failing to set the username in the form DOMAIN\username will result in the error:
org.apache.commons.httpclient.auth.InvalidCredentialsException: 

Credentials cannot be used for NTLM authentication:
org.apache.commons.httpclient.UsernamePasswordCredentials
at org.apache.commons.httpclient.auth.NTLMScheme.authenticate(NTLMScheme.java:332)
at org.apache.commons.httpclient.HttpMethodDirector.authenticateHost(HttpMethodDirector.java:282)
at org.apache.commons.httpclient.HttpMethodDirector.authenticate(HttpMethodDirector.java:234)
at org.apache.commons.httpclient.HttpMethodDirector.executeMethod(HttpMethodDirector.java:170)
at org.apache.commons.httpclient.HttpClient.executeMethod(HttpClient.java:397)
at org.apache.axis.transport.http.CommonsHTTPSender.invoke(CommonsHTTPSender.java:186)
This is because the format of the username determines the Credentials instance created. With the DOMAIN\... prefix on the username you get an instance of org.apache.commons.httpclient.NTCredentials rather than org.apache.commons.httpclient.UsernamePasswordCredentials - which as the message explains can't be used for NTLM.