hc-dev mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From Kiss Gergely <kiss.gerg...@gmail.com>
Subject NegotiateAuth for HttpClient 4.x
Date Wed, 18 Nov 2009 09:03:54 GMT
Dear HttpComponent Developers,

We're using HttpComponents 4.x in out project for some time now, and last
week I spent a lot of time figuring out how Exchange works with WebDAV and
Kerberos authentication.
In the meantime, I have implemented the NegotiateScheme class for 4.x -
based on Mikael Wikstrom's previous work for HttpClient 3.x - which I'd like
to contribute back to the community.

Notes:
- with 4.x it's a bit harder to add a new authentication scheme, but is
possible with DefaultHttpClient.setTargetAuthenticationHandler() - so the
new authPreferences should look like { "negotiate", "ntlm", "digest",
"basic" }
- unfortunately the current (4.0) implementation does not fall back to Basic
or Digest if Negotiate or NTLM authentication failed, so you have to decide
which one to use before executing the request
- The execute() call is required to run in a JAAS context (with
Subject.doAs(...))
- Kerberos authentication requires a service name to work (the first part of
the SPN), and this was a constant value ("HTTP") in the previous version -
but the target service may already have another SPN (so registering HTTP
would be unnecessary). For this reason, I introduced the
parameter NegotiateSchemeFactory.SERVICE_PREFIX, which is read from the
HttpParams specified to the client.
- Credential delegation was tested and works very nicely

Best regards
Gergely Kiss


===========================================
NegotiateSchemeFactory.java:
===========================================
package org.apache.http.impl.auth;

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


/**
 * Negotiate scheme factory for HttpClient 4.x.
 *
 * @author  <a href="mailto:kiss.gergely@gmail.com">Gergely Kiss</a>
 */
public class NegotiateSchemeFactory implements AuthSchemeFactory {

    /**
     * Service prefix for the Kerberos SPN.
     *
     * <p>This is HTTP by default, but some services may already have
another SPN.</p>
     */
    public static final String SERVICE_PREFIX = "KerberosServiceName";


    public AuthScheme newInstance(HttpParams params) {
        NegotiateScheme scheme = new NegotiateScheme();

        // Setting the service prefix, if specified
        Object param = params.getParameter(SERVICE_PREFIX);

        if (param != null) {
            scheme.setServicePrefix(String.valueOf(param));
        }

        return scheme;
    }
}


===========================================
NegotiateScheme.java:
===========================================
package org.apache.http.impl.auth;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.apache.http.Header;
import org.apache.http.HttpRequest;
import org.apache.http.auth.AUTH;
import org.apache.http.auth.AuthScheme;
import org.apache.http.auth.AuthenticationException;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.InvalidCredentialsException;
import org.apache.http.auth.MalformedChallengeException;
import org.apache.http.message.BasicHeader;

import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSException;
import org.ietf.jgss.GSSManager;
import org.ietf.jgss.GSSName;
import org.ietf.jgss.Oid;


/**
 * Authentication scheme implementing the Negotiate protocol with JAAS for
HTTPClient 4.x.
 *
 * @author  <a href="mailto:mikael.wikstrom@it.su.se">Mikael Wikstrom</a>
 * @author  <a href="mailto:kiss.gergely@gmail.com">Gergely Kiss</a>
 */
public class NegotiateScheme implements AuthScheme {

    /** Log object for this class. */
    private static final Log log = LogFactory.getLog(NegotiateScheme.class);

    private static final int UNINITIATED = 0;
    private static final int INITIATED = 1;
    private static final int NEGOTIATING = 3;
    private static final int ESTABLISHED = 4;
    private static final int FAILED = Integer.MAX_VALUE;


    private GSSContext context = null;

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

    /** base64 decoded challenge * */
    byte[] token = new byte[0];

    /**
     * Service prefix for the Kerberos SPN.
     *
     * <p>This is usually HTTP, but another service name can also be
used.</p>
     */
    private String servicePrefix = "HTTP";

    /**
     * Default constructor for the Negotiate authentication scheme.
     *
     * @param  subject
     *
     * @since  3.0
     */
    public NegotiateScheme() {
        super();
        state = UNINITIATED;
    }

    /**
     * Init GSSContext for negotiation.
     *
     * @param  host  servername only (e.g: radar.it.su.se)
     */
    protected void init(String host) throws Exception {
        log.debug("init " + host);

        /* Kerberos v5 GSS-API mechanism defined in RFC 1964. */
        Oid krb5Oid = new Oid("1.2.840.113554.1.2.2");
        GSSManager manager = GSSManager.getInstance();
        GSSName serverName = manager.createName(servicePrefix + "/" + host,
null);
        context = manager.createContext(serverName, krb5Oid, null,
GSSContext.DEFAULT_LIFETIME);
        context.requestCredDeleg(true);
        context.requestMutualAuth(true);

        state = INITIATED;
    }

    /**
     * Processes the Negotiate challenge.
     *
     * @param  challenge  the challenge string
     *
     * @since  4.0
     */
    public void processChallenge(Header header) throws
MalformedChallengeException {
        String challenge = header.getValue();
        log.debug("enter processChallenge(challenge=\"" + challenge +
"\")");

        if (challenge.startsWith("Negotiate")) {

            if (!isComplete()) {
                state = NEGOTIATING;
            }

            if (challenge.startsWith("Negotiate ")) {
                token = new
Base64().decode(challenge.substring(10).getBytes());
            } else {
                token = new byte[0];
            }
        }
    }

    /**
     * Tests if the Negotiate authentication process has been completed.
     *
     * @return  <tt>true</tt> if authorization has been processed,
<tt>false</tt> otherwise.
     *
     * @since   3.0
     */
    public boolean isComplete() {
        log.debug("enter isComplete()");

        return (this.state == ESTABLISHED) || (this.state == FAILED);
    }

    /**
     * Returns textual designation of the Negotiate authentication scheme.
     *
     * @return  <code>Negotiate</code>
     */
    public String getSchemeName() {
        return "Negotiate";
    }

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

    /**
     * Returns the authentication parameter with the given name, if
available.
     *
     * <p>There are no valid parameters for Negotiate authentication so this
method always returns <tt>null</tt>.</p>
     *
     * @param   name  The name of the parameter to be returned
     *
     * @return  the parameter with the given name
     */
    public String getParameter(String name) {
        log.debug("enter getParameter(" + name + ")");

        if (name == null) {
            throw new IllegalArgumentException("Parameter name may not be
null");
        }

        return null;
    }

    /**
     * Returns <tt>true</tt>. Negotiate authentication scheme is connection
based.
     *
     * @return  <tt>true</tt>.
     *
     * @since   3.0
     */
    public boolean isConnectionBased() {
        log.info("enter isConnectionBased()");

        return true;
    }

    /**
     * Produces Negotiate authorization string based on token created by
processChallenge.
     *
     * @param   credentials  Never used be the Negotiate scheme but must be
provided to satisfy common-httpclient API.
     *                       Credentials from JAAS will be used insted.
     * @param   method       The method being authenticated
     *
     * @return  an Negotiate authorization string
     *
     * @throws  AuthenticationException  if authorization string cannot be
generated due to an authentication failure
     *
     * @since   4.0
     */
    public Header authenticate(Credentials credentials, HttpRequest request)
        throws AuthenticationException {
        log.debug("enter NegotiateScheme.authenticate(Credentials,
HttpMethod)");

        if (state == UNINITIATED) {
            throw new IllegalStateException(
                "Negotiation authentication process has not been
initiated");
        }

        try {

            if (context == null) {
                Header host = request.getFirstHeader("Host");

                if (host != null) {
                    log.info("host: " + host);
                    init(host.getValue());
                } else {
                    throw new AuthenticationException(
                        "Failed to get host name from header parameters");
                }
            }
        } catch (AuthenticationException e) {
            throw e;
        } catch (Exception e) {
            log.error(e.getMessage());
            log.debug("Failure trace", e);
            state = FAILED;
            throw new AuthenticationException(e.getMessage());
        }

        try {

            // HTTP 1.1 issue:
            // Mutual auth will never complete do to 200 insted of 401 in
            // return from server. "state" will never reach ESTABLISHED
            // but it works anyway
            token = context.initSecContext(token, 0, token.length);
            log.info("got token, sending " + token.length + " to server");
        } catch (GSSException e) {
            log.error(e.getMessage());
            log.debug("Failure trace", e);
            state = FAILED;

            if ((e.getMajor() == GSSException.DEFECTIVE_CREDENTIAL) ||
                    (e.getMajor() == GSSException.CREDENTIALS_EXPIRED)) {
                throw new InvalidCredentialsException(e.getMessage(), e);
            }

            if (e.getMajor() == GSSException.NO_CRED) {
                throw new InvalidCredentialsException(e.getMessage(), e);
            }

            if ((e.getMajor() == GSSException.DEFECTIVE_TOKEN) ||
                    (e.getMajor() == GSSException.DUPLICATE_TOKEN) ||
                    (e.getMajor() == GSSException.OLD_TOKEN)) {
                throw new InvalidCredentialsException(e.getMessage(), e);
            }

            // other error
            throw new AuthenticationException(e.getMessage());
        }

        return new BasicHeader(AUTH.WWW_AUTH_RESP,
                "Negotiate " + new String(new Base64().encode(token)));
    }

    public void setServicePrefix(String servicePrefix) {
        this.servicePrefix = servicePrefix;
    }
}

Mime
  • Unnamed multipart/alternative (inline, None, 0 bytes)
View raw message