tags:

views:

1101

answers:

6

I need to connect Apache Axis 1.4 to a Webservice that uses NTLM authentication to restrict access to its operations. I'm expecting to use Samba Jcifs to handle the NTLM handshake.

I found

http://hc.apache.org/httpcomponents-client/ntlm.html

which gives me fantastic directions for how to wire up HttpClient 4.0 with jcifs.

Trouble is, Axis wants to use Http Client 3.0 and the two apis look very different.

There are 2 possibilities that I can see

  1. Write an object for Axis that lets it plug into HttpClient 4.
  2. Figure out how to wire HttpClient 3.0 up with Samba Jcifs.

Number 1. looks non-trivial, but possible Number 2. I cannot find any encouraging messages on the web describing how to do this.

My question is: has anyone successfully connected samba jcifs with HttpClient 3.0 ? Has anyone already created an Axis HttpSender object that works with HttpClient 4 ?

Is there some better alternative that I have not considered?

+1  A: 

Finally have a solution to this.

The problem

Apache Axis uses Apache HTTP Client which provides its own NTLM implementation.
However this implemention is incomplete; it only supports the primitive LM authentication.
The system I need to connect to insists upon the more recent NTLM authentication.

Therefore my Webservice was failing to authenticate when using the Apache HTTP Client with NTLM.

This actually then enters an infinite loop as the HTTPClient will never stop trying and failing to authenticate.

The solution

jcifs fully supports all 3 versions of the NTLM handshake.
I have copy-and-pasted org.apache.commons.httpclient.auth.NTLM into my own class (it is declared as 'final' in order to defeat inheritance)

I have then overwritten the method

public String getType3Message( String user, String password, String host, String domain, byte[] nonce) throws AuthenticationException

to construct an instance of jcifs.ntlmssp.Type3Message and use this object to return a Type3Message that has the NTML authentication correctly generated.

I then needed to create my own instance of org.apache.commons.httpclient.auth.AuthScheme to make use of this new NTLM implementation. call

org.apache.commons.httpclient.auth.AuthPolicy.registerAuthScheme(AuthPolicy.NTLM, MyNewAuthScheme.class)

start up my WS endpoint stub.

And it works !!!

Ben Hammond
+1  A: 

Hello Ben. I'm struggling the same problem now. I have created my Auth scheme class and successfullt registered it. Also I replaced type3Message generationwith jcifs. But still get "401.1 Unauthorised". Seems like correct messages are still not sent to the server. Maybe I missed some parameters when generating type3message with jcifs!? I use axis2. Can you post some code here or send it to email ([email protected]). Really need some help:-).

Sergey
Hi Sergey. I have posted the sourcecode.All I can really suggest is that you* check the logs in your authenticating server. Is it explicity receiving (and rejecting) the credentials* put break points in JcifsNtlmScheme.authenticate. Which stage of the handshake is failing ? What level of protocol is your authenticating server expecting to see ?
Ben Hammond
That's great!!! It works!!!Actually I'm .NET developer and new to java. I've spent several days or even more in google, trying to find any appropriate solution. But no results.Thank you very much for your response!!!
Sergey
A: 

In response to Sergey's comment...

I have two clases in my solution. An Authorisation Scheme like this


import java.io.IOException;

import jcifs.ntlmssp.Type1Message;
import jcifs.ntlmssp.Type2Message;
import jcifs.ntlmssp.Type3Message;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.httpclient.Credentials;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.NTCredentials;
import org.apache.commons.httpclient.auth.AuthChallengeParser;
import org.apache.commons.httpclient.auth.AuthScheme;
import org.apache.commons.httpclient.auth.AuthenticationException;
import org.apache.commons.httpclient.auth.InvalidCredentialsException;
import org.apache.commons.httpclient.auth.MalformedChallengeException;
import org.apache.commons.httpclient.auth.NTLMScheme;
import org.apache.commons.httpclient.util.EncodingUtil;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * AuthScheme that delegates the work of reading and writing NTLM messages to
 * the JCIFS implementation
 * 
 * directly inspired by org.apache.commons.httpclient.auth.NTLMScheme
 * 
 *
 * This software is based upon voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * .
 *
 */
public class JcifsNtlmScheme implements AuthScheme
{
    /** Log object for this class. */
    private static final Log LOG = LogFactory.getLog(NTLMScheme.class);

    /** NTLM challenge string. */
    private String ntlmchallenge = null;

    private static final int UNINITIATED = 0;
    private static final int INITIATED = 1;
    private static final int TYPE1_MSG_GENERATED = 2;
    private static final int TYPE2_MSG_RECEIVED = 3;
    private static final int TYPE3_MSG_GENERATED = 4;
    private static final int FAILED = Integer.MAX_VALUE;

    /** Authentication process state */
    private int state;

    /**
     * Default constructor for the NTLM authentication scheme.
     * 
     * @since 3.0
     */
    public JcifsNtlmScheme()
    {
     super();
     this.state = UNINITIATED;
    }

    /**
     * Constructor for the NTLM authentication scheme.
     * 
     * @param challenge
     *            The authentication challenge
     * 
     * @throws MalformedChallengeException
     *             is thrown if the authentication challenge is malformed
     */
    public JcifsNtlmScheme(final String challenge)
      throws MalformedChallengeException
    {
     super();
     processChallenge(challenge);
    }

    /**
     * Processes the NTLM challenge.
     * 
     * @param challenge
     *            the challenge string
     * 
     * @throws MalformedChallengeException
     *             is thrown if the authentication challenge is malformed
     * 
     * @since 3.0
     */
    public void processChallenge(final String challenge)
      throws MalformedChallengeException
    {
     String s = AuthChallengeParser.extractScheme(challenge);
     if (!s.equalsIgnoreCase(getSchemeName()))
     {
      throw new MalformedChallengeException("Invalid NTLM challenge: "
        + challenge);
     }
     int i = challenge.indexOf(' ');
     if (i != -1)
     {
      s = challenge.substring(i, challenge.length());
      this.ntlmchallenge = s.trim();
      this.state = TYPE2_MSG_RECEIVED;
     }
     else
     {
      this.ntlmchallenge = "";
      if (this.state == UNINITIATED)
      {
       this.state = INITIATED;
      }
      else
      {
       this.state = FAILED;
      }
     }
    }

    /**
     * Tests if the NTLM authentication process has been completed.
     * 
     * @return true if Basic authorization has been processed,
     *         false otherwise.
     * 
     * @since 3.0
     */
    public boolean isComplete()
    {
     return this.state == TYPE3_MSG_GENERATED || this.state == FAILED;
    }

    /**
     * Returns textual designation of the NTLM authentication scheme.
     * 
     * @return ntlm
     */
    public String getSchemeName()
    {
     return "ntlm";
    }

    /**
     * The concept of an authentication realm is not supported by the NTLM
     * authentication scheme. Always returns null.
     * 
     * @return null
     */
    public String getRealm()
    {
     return null;
    }

    /**
     * Unsupported.
     */
    public String getID()
    {
     throw new UnsupportedOperationException();
    }

    /**
     * Returns the authentication parameter with the given name, if available.
     * 
     * 

* There are no valid parameters for NTLM authentication so this method * always returns null. *

* * @param name * The name of the parameter to be returned * * @return the parameter with the given name */ public String getParameter(String name) { if (name == null) { throw new IllegalArgumentException("Parameter name may not be null"); } return null; } /** * Returns true. NTLM authentication scheme is connection based. * * @return true. * * @since 3.0 */ public boolean isConnectionBased() { return true; } /** * Unsupported. */ public static String authenticate( final NTCredentials credentials, final String challenge) throws AuthenticationException { throw new UnsupportedOperationException(); } /** * Unsupported. */ public static String authenticate( final NTCredentials credentials, final String challenge, String charset) throws AuthenticationException { throw new UnsupportedOperationException(); } /** * Unsupported. */ public String authenticate( Credentials credentials, String method, String uri) throws AuthenticationException { throw new UnsupportedOperationException(); } /** * Produces NTLM authorization string for the given set of * {@link Credentials}. * * @param credentials * The set of credentials to be used for athentication * @param method * The method being authenticated * * @throws InvalidCredentialsException * if authentication credentials are not valid or not applicable * for this authentication scheme * @throws AuthenticationException * if authorization string cannot be generated due to an * authentication failure * * @return an NTLM authorization string * * @since 3.0 */ public String authenticate(Credentials credentials, HttpMethod method) throws AuthenticationException { LOG.trace("enter NTLMScheme.authenticate(Credentials, HttpMethod)"); if (this.state == UNINITIATED) { throw new IllegalStateException( "NTLM authentication process has not been initiated"); } NTCredentials ntcredentials = null; try { ntcredentials = (NTCredentials) credentials; } catch (ClassCastException e) { throw new InvalidCredentialsException( "Credentials cannot be used for NTLM authentication: " + credentials.getClass().getName()); } byte[] msgBytes = null; String response = null; if (this.state == INITIATED) { Type1Message msg = new Type1Message(); // @see http://davenport.sourceforge.net/ntlm.html#theType1Message // dont' support Unicode // negotiate OEM // request authentication realm in Type2 response // not signed // not encrypted // not authenticated // no lan manager key // negotiate NTLM msg.setFlags(0x5206); msg.setSuppliedWorkstation(ntcredentials.getHost()); msg.setSuppliedDomain(ntcredentials.getDomain()); msgBytes = msg.toByteArray(); this.state = TYPE1_MSG_GENERATED; } else if (this.state == TYPE2_MSG_RECEIVED) { byte[] msg2Bytes = Base64.decodeBase64(EncodingUtil.getBytes( this.ntlmchallenge, method.getParams().getCredentialCharset())); try { Type2Message msg2 = new Type2Message(msg2Bytes); Type3Message msg3 = new Type3Message(msg2, ntcredentials.getPassword(), ntcredentials.getDomain(), ntcredentials .getUserName(), ntcredentials.getHost()); msgBytes = msg3.toByteArray(); } catch (IOException ex) { throw new AuthenticationException( "unable to parse Type2Message", ex); } this.state = TYPE3_MSG_GENERATED; } else { throw new RuntimeException("failed to authenticate"); } response = EncodingUtil.getAsciiString(Base64.encodeBase64(msgBytes)); return "NTLM " + response; } }

And a class to register the authorisation scheme, like this


import org.apache.commons.httpclient.Credentials;
import org.apache.commons.httpclient.NTCredentials;
import org.apache.commons.httpclient.auth.AuthPolicy;
import org.apache.commons.httpclient.auth.AuthScheme;
import org.apache.commons.httpclient.auth.CredentialsNotAvailableException;
import org.apache.commons.httpclient.auth.CredentialsProvider;
import org.apache.commons.httpclient.params.DefaultHttpParams;
import org.apache.commons.httpclient.params.DefaultHttpParamsFactory;
import org.apache.commons.httpclient.params.HttpParams;

/**
 * registers NTLM authentication for apache axis
 * 
 */
public class NtlmJcifsCredentials
{
    public static void register(String password)
    {
     final String username = System.getProperty("user.name");
     final String computername = System.getenv("COMPUTERNAME");
     final String userDomain = System.getenv("USERDOMAIN");
     register(username, password, computername, userDomain);
    }
    public static void register(String username, String password, String userDomain)
    {
     final String computername = System.getenv("COMPUTERNAME");
     register(username, password, computername, userDomain);
    }

    public static void register(
      String username, String password, String computername, String domain)
    {
     final NTCredentials ntCred =
       new NTCredentials(username, password, computername, domain);

     final CredentialsProvider ntlmCredProvider = new CredentialsProvider()
     {
      public Credentials getCredentials(
        AuthScheme scheme, String host, int port, boolean proxy)
        throws CredentialsNotAvailableException
      {
       return ntCred;
      }
     };
     final DefaultHttpParamsFactory paramFact =
       new DefaultHttpParamsFactory()
       {
        @Override
        protected HttpParams createParams()
        {
         HttpParams htp = super.createParams();
         htp.setParameter(
           CredentialsProvider.PROVIDER,
           ntlmCredProvider);
         return htp;
        }
       };
     DefaultHttpParams.setHttpParamsFactory(paramFact);

     // we want all our jcifs encoding to be ascii
     jcifs.Config.setProperty("jcifs.encoding", "ASCII");

     // our jcifs implemented NTLM is required for MDW's authentication
     AuthPolicy.registerAuthScheme(AuthPolicy.NTLM, JcifsNtlmScheme.class);
    }
}

At runtime I call

 NtlmJcifsCredentials.register(username, password, domain) 

I construct my endpoint stub, and it just works. As a beneficial side-effect this will simply chuck an exception if if fails to authenticate - the Default Apache Commons class will keep trying to connect infinitely - which in the case of NTLM can easily lead to your account being locked out from windows.

Ben Hammond
A: 

If the web service you want to connect to supports integrated windows authentication, there is an open source library, http://spnego.sourceforge.net/protected_soap_service.html, that has a client that might be able to get you what you need.

I heard that MS will be deprecating NTLM in favor of Kerberos.

Pat Gonzalez
A: 

I've got it working, but I did NOT yet implement proxy server support in the HTTP. http://www.magsoft.nl/share/Axis2%20patch.zip All the jars I use are in the project lib directory. There are some class path requirements. First the Axis2 HTTPClient4 patch.jar has to be above the axis jars. Furthermore, commons-httpclient-3.1.jar needs to be still in the classpath, but after the httpclient-4 jars.

Here is how I implemented the client:


Scheme http = new Scheme("http", PlainSocketFactory.getSocketFactory(), 80);
SchemeRegistry sr = new SchemeRegistry();
sr.register(http);
HttpParams httpParms = new BasicHttpParams();
ClientConnectionManager connManager = new ThreadSafeClientConnManager(httpParms, sr);
DefaultHttpClient httpclient = new DefaultHttpClient(connManager, httpParms);
httpclient.getAuthSchemes().register(HttpTransportProperties.Authenticator.NTLM, new NTLMSchemeFactory());
httpclient.getCredentialsProvider().setCredentials(new AuthScope(host, -1), new NTCredentials(user, pass, host, domain));

sps = new SharepointServiceStub(addr.toString());

List authScheme = new ArrayList();
authScheme.add(HttpTransportProperties.Authenticator.NTLM);
HttpTransportProperties.Authenticator auth = new HttpTransportProperties.Authenticator();
auth.setHost(host);
auth.setDomain(domain);
auth.setUsername(user);
auth.setPassword(pass);
auth.setAuthSchemes(authScheme);
Options options = sps._getServiceClient().getOptions();
options.setProperty(org.apache.axis2.transport.http.HTTPConstants.REUSE_HTTP_CLIENT, true);
options.setProperty(org.apache.axis2.transport.http.HTTPConstants.CACHED_HTTP_CLIENT, httpclient);
options.setProperty(org.apache.axis2.transport.http.HTTPConstants.AUTHENTICATE, auth);
options.setProperty(org.apache.axis2.transport.http.HTTPConstants.CHUNKED, Boolean.TRUE);
options.setProperty(org.apache.axis2.transport.http.HTTPConstants.CONNECTION_TIMEOUT, 900000); // 15 minutes
options.setProperty(org.apache.axis2.transport.http.HTTPConstants.SO_TIMEOUT, 180000); // 3 minutes

But for this to work you will need the following tree classes: NTLMSchemeFactory.java


package ...;

import org.apache.http.auth.AuthScheme;
import org.apache.http.auth.AuthSchemeFactory;
import org.apache.http.impl.auth.NTLMScheme;
import org.apache.http.params.HttpParams;

public class NTLMSchemeFactory implements AuthSchemeFactory
{

    public NTLMSchemeFactory()
    {
    }

    public AuthScheme newInstance(final HttpParams params)
    {
        return new NTLMScheme(new JCIFSEngine());
    }

}

JCIFSScheme.java


package ...;

import org.apache.http.impl.auth.NTLMScheme;



public class JCIFSScheme extends NTLMScheme
{
    public JCIFSScheme()
    {
        super(new JCIFSEngine());
    }
}

JCIFSEngine.java


package ...;

import java.io.IOException;

import jcifs.ntlmssp.Type1Message;
import jcifs.ntlmssp.Type2Message;
import jcifs.ntlmssp.Type3Message;
import jcifs.util.Base64;

import org.apache.http.impl.auth.NTLMEngine;
import org.apache.http.impl.auth.NTLMEngineException;

public class JCIFSEngine implements NTLMEngine
{
    public String generateType1Msg(String domain, String workstation) throws NTLMEngineException
    {

        Type1Message t1m = new Type1Message(Type1Message.getDefaultFlags(), domain, workstation);
        return Base64.encode(t1m.toByteArray());
    }


    public String generateType3Msg(String username, String password, String domain, String workstation, String challenge)
            throws NTLMEngineException
    {
        Type2Message t2m;
        try
        {
            t2m = new Type2Message(Base64.decode(challenge));
        } catch (IOException ex)
        {
            throw new NTLMEngineException("Invalid Type2 message", ex);
        }
        Type3Message t3m = new Type3Message(t2m, password, domain, username, workstation, 0);
        return Base64.encode(t3m.toByteArray());
    }

}
Sander Postma
A: 

Thank yo very much Ben, good work. For my solution I need 2 improvements, based on your classes.

1) class JcifsNtlmScheme

The interface has changed in jcifs (I use version 1.3.14). The NTLM flag is required, I'm not really sure but 0x82 works for me.

int flags = Type3Message.NTLMSSP_NEGOTIATE_OEM | Type3Message.NTLMSSP_NEGOTIATE_LM_KEY;

Type3Message msg3 =
        new Type3Message(msg2, ntcredentials.getPassword(),
        ntcredentials.getDomain(), ntcredentials.getUserName(), ntcredentials.getHost(), flags);

2) class NtlmJcifsCredentials

 DefaultHttpParams.setHttpParamsFactory(paramFact);

This works fine for the first connection. It seems to be a global setting. It's probably not really thread-safe. I need the credentials on connections base. So I dropped this class and inserted the built-in Authenticator directly after creation of the webservice stub:

jcifs.Config.setProperty("jcifs.encoding", "ASCII");
AuthPolicy.registerAuthScheme(AuthPolicy.NTLM, JcifsNtlmScheme.class);

Authenticator authenticator = new Authenticator();
List<String> authScheme = new ArrayList<String>();
authScheme.add(Authenticator.NTLM);
authScheme.add(Authenticator.BASIC);
authenticator.setAuthSchemes(authScheme);
authenticator.setUsername(myusername);
authenticator.setPassword(mypassword);
authenticator.setHost(servername);
authenticator.setDomain(domain);        
exService._getServiceClient().getOptions().setProperty(HTTPConstants.AUTHENTICATE, authenticator);       
exService._getServiceClient().getOptions().setProperty(HTTPConstants.CHUNKED, Boolean.FALSE);
exService._getServiceClient().getOptions().setProperty(HTTPConstants.REUSE_HTTP_CLIENT, Boolean.TRUE);
roman sidler