com.example.ManualSpnegoNegotiateServlet.java Source code

Java tutorial

Introduction

Here is the source code for com.example.ManualSpnegoNegotiateServlet.java

Source

/**
 *   A short prefatory note.
 *
 *   Large parts of this file are based on[1, 2], and I also use
 *  important parts of [3, 4]. Kudos to those guys, they made this
 *  possible, I just combined the ingredients.
 *
 *   I basically removed the wicket stuff from [1] (distracted from
 *  the goal; explain the underlying mechanisms of Spnego) and turned
 *  it into a bare-bones HttpServlet.
 *
 *  Then I changed the serverside credential part a bit so that
 *  it doesn't need a keytab file --which just adds another step in
 *  an already complex mix of elements (i.e. how to make a correct
 *  keytab file (key version, encryption types, SPNs... a mismatch
 *  in any of these and the whole authentication would have failed
 *  miserably)). So, instead of a keytab file, for the sake of
 *  simplicity, this example servlet simply uses a hard-coded
 *  username/password combination.
 * 
 *  Furthermore, I also show how to use the jaaslounge-decoding
 *  package[3] to retrieve the Microsoft PAC authorization data
 *  from an Active Directory issued Kerberos ticket. For instance,
 *  to read the Group SIDs of the authenticated user.
 *
 *  __
 *  Marcel Toele, Spring 2012.
 *
 * [1] - https://cwiki.apache.org/DIRxINTEROP/kerberos-authentication-to-wicket.html
 * [2] - http://spnego.sourceforge.net/
 * [3] - http://jaaslounge.sourceforge.net/
 * [4] - http://www.bouncycastle.org/
 */

/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */
package com.example;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.Principal;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.Arrays;
import javax.security.auth.Subject;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.kerberos.KerberosKey;
import javax.security.auth.kerberos.KerberosPrincipal;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.sourceforge.spnego.Base64;
import net.sourceforge.spnego.SpnegoProvider;
import org.apache.commons.collections.IteratorUtils;
import org.ietf.jgss.*;
import org.jaaslounge.decoding.DecodingException;
import org.jaaslounge.decoding.kerberos.KerberosAuthData;
import org.jaaslounge.decoding.kerberos.KerberosPacAuthData;
import org.jaaslounge.decoding.kerberos.KerberosToken;
import org.jaaslounge.decoding.pac.PacSid;
import org.jaaslounge.decoding.spnego.SpnegoToken;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Simple example of an access protected hello page that processes SPNEGO GSS-API Kerberos authentication.
 *
 * This class basically is the same as the FilteredHelloSpnegoServlet, but exposes the underlying 
 * mechanisms that are otherwise handled by the spnego-filter servlet. (So, if you use the spnego-filter,
 * you *DO NOT NEED ALL THIS CODE* !!!)
 */
public class ManualSpnegoNegotiateServlet extends HttpServlet {

    private static final Logger log = LoggerFactory.getLogger(ManualSpnegoNegotiateServlet.class);
    /**
     * Constant for the header lead for the unsupported NTLM mechanism.
     */
    private static final byte NTLMSSP[] = { (byte) 0x4E, (byte) 0x54, (byte) 0x4C, (byte) 0x4D, (byte) 0x53,
            (byte) 0x53, (byte) 0x50 };

    /**
     * ManualSpnegoNegotiateServlet.
     */
    public ManualSpnegoNegotiateServlet() {
        System.setProperty("java.security.auth.login.config", "src/main/conf/spnego.conf");
        System.setProperty("javax.security.auth.useSubjectCredsOnly", "false");
        System.setProperty("sun.security.spnego.msinterop", "true"); // true by default
        System.setProperty("sun.security.spnego.debug", "false"); // false by default
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("text/html");
        response.getWriter().println("<h1>Manual SPNEGO Negotiate Hello Servlet</h1>");
        response.getWriter().println("<pre>");
        try {
            if (attemptNegotiation(request, response)) {
                response.setStatus(HttpServletResponse.SC_OK);
                response.getWriter().println("Authenticated.");
            } else {
                response.getWriter().println("Authentication failed, sorry.");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        response.getWriter().println("</pre>");
    }

    /**
     * Use of Kerberos is wrapped in an HTTP auth-scheme of "Negotiate" [RFC 4559].
     *
     * The auth-params exchanged use data formats defined for use with the GSS-API [RFC 2743]. In particular, they follow the formats set for the SPNEGO [RFC 4178] and
     * Kerberos [RFC 4121] mechanisms for GSSAPI. The "Negotiate" auth-scheme calls for the use of SPNEGO GSSAPI tokens that the specific mechanism type specifies.
     *
     * The current implementation of this protocol is limited to the use of SPNEGO with the Kerberos protocol.
     *
     * @param request
     * @param response
     * @throws ServletException
     *
     * @return true upon successful authentication, false otherwise
     */
    protected boolean attemptNegotiation(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, UnsupportedEncodingException, IOException {
        log.debug("Attempting negotiation.");

        String header = request.getHeader("Authorization");

        /**
         * Guard clause to check for Negotiate header.
         *
         * If the server receives a request for an access-protected object, and if an acceptable Authorization header has not been sent, the server responds with a "401
         * Unauthorized" status code, and a "WWW-Authenticate:" header as per the framework described in [RFC 2616]. The initial WWW-Authenticate header will not carry
         * any gssapi-data.
         */
        if (header == null || header.length() < 10 || !header.startsWith("Negotiate ")) {
            response.setHeader("WWW-Authenticate", "Negotiate");
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            log.debug("Proper authorization header not found, returning challenge.");
            return false;
        }

        /**
         * A client may initiate a connection to the server with an "Authorization" header containing the initial token for the server. This form will bypass the initial
         * 401 error from the server when the client knows that the server will accept the Negotiate HTTP authentication type.
         */
        log.debug("Authorization header found, continuing negotiation.");

        /**
         * The data following the word Negotiate is the GSS-API data to process.
         */
        byte gssapiData[] = Base64.decode(header.substring(10));

        log.debug("GSS API data: " + Arrays.toString(gssapiData));

        /**
         * Guard clause to check for the unsupported NTLM authentication mechanism.
         */
        if (isNtlmMechanism(gssapiData)) {
            log.warn("Got request for unsupported NTLM mechanism, aborting negotiation.");
            return false;
        }

        /**
         * The server attempts to establish a security context. Establishment may result in tokens that the server must return to the client. Tokens are BASE-64 encoded
         * GSS-API data.
         */
        GSSContext gssContext = null;
        LoginContext loginContext = null;
        String outToken = null;

        try {
            final String domainUsername = "Zeus";
            final String domainUserPassword = "Z3usP@55";
            final CallbackHandler handler = SpnegoProvider.getUsernamePasswordHandler(domainUsername,
                    domainUserPassword);

            loginContext = new LoginContext("spnego-server", handler);
            loginContext.login();
            Subject subject = loginContext.getSubject();

            Oid spnegoOid = new Oid("1.3.6.1.5.5.2"); // for spnego answers
            Oid kerbv5Oid = new Oid("1.2.840.113554.1.2.2"); // for chromium (they send a kerbv5 token instead of spnego)
            final Oid[] oids = new Oid[] { spnegoOid, kerbv5Oid };

            final GSSManager manager = GSSManager.getInstance();
            final PrivilegedExceptionAction<GSSCredential> action = new PrivilegedExceptionAction<GSSCredential>() {
                public GSSCredential run() throws GSSException {
                    return manager.createCredential(null, GSSCredential.INDEFINITE_LIFETIME, oids,
                            GSSCredential.ACCEPT_ONLY);
                }
            };

            GSSCredential serverCreds = Subject.doAs(subject, action);

            log.debug("Mechs: " + Arrays.toString(serverCreds.getMechs()));

            gssContext = manager.createContext(serverCreds);

            log.debug("Context created. " + gssContext);

            byte tokenBytes[] = gssContext.acceptSecContext(gssapiData, 0, gssapiData.length);
            outToken = Base64.encode(tokenBytes);
        } catch (PrivilegedActionException ex) {
            log.error("", ex);
        } catch (LoginException ex) {
            log.error("", ex);
        } catch (GSSException gsse) {
            gsse.printStackTrace();
            log.error("GSSException:       " + gsse.getMessage());
            log.error("GSSException major: " + gsse.getMajorString());
            log.error("GSSException minor: " + gsse.getMinorString());
            throw new ServletException(gsse);
        }

        /**
         * If the context is established, we can attempt to retrieve the name of the "context initiator." In the case of the Kerberos mechanism, the context initiator is
         * the Kerberos principal of the client. Additionally, the client may be delegating credentials.
         */
        if (gssContext != null && gssContext.isEstablished()) {
            log.debug("Context established, attempting Kerberos principal retrieval.");

            try {
                Subject subject = new Subject();
                GSSName clientGSSName = gssContext.getSrcName();
                KerberosPrincipal clientPrincipal = new KerberosPrincipal(clientGSSName.toString());
                subject.getPrincipals().add(clientPrincipal);
                log.info("Got client Kerberos principal: " + clientGSSName);
                response.getWriter().println("Hello, " + clientPrincipal);

                /**
                 * Retrieve LogonInfo (for example, GroupSIDs) from the PAC Authorization Data
                 * from a Kerberos Ticket that was issued by Active Directory.
                 */
                byte[] kerberosTokenData = gssapiData;
                try {
                    SpnegoToken token = SpnegoToken.parse(gssapiData);
                    kerberosTokenData = token.getMechanismToken();
                } catch (DecodingException dex) {
                    // Chromium bug: sends a Kerberos response instead of an spnego response with a Kerberos mechanism
                } catch (Exception ex) {
                    log.error("", ex);
                }

                try {
                    Object[] keyObjs = IteratorUtils
                            .toArray(loginContext.getSubject().getPrivateCredentials(KerberosKey.class).iterator());
                    KerberosKey[] keys = new KerberosKey[keyObjs.length];
                    System.arraycopy(keyObjs, 0, keys, 0, keyObjs.length);

                    KerberosToken token = new KerberosToken(kerberosTokenData, keys);
                    log.info("Authorizations: ");
                    for (KerberosAuthData authData : token.getTicket().getEncData().getUserAuthorizations()) {
                        if (authData instanceof KerberosPacAuthData) {
                            PacSid[] groupSIDs = ((KerberosPacAuthData) authData).getPac().getLogonInfo()
                                    .getGroupSids();
                            log.info("GroupSids: " + Arrays.toString(groupSIDs));
                            response.getWriter().println("Found group SIDs: " + Arrays.toString(groupSIDs));
                        } else {
                            log.info("AuthData without PAC: " + authData.toString());
                        }
                    }
                } catch (Exception ex) {
                    log.error("", ex);
                }

                if (gssContext.getCredDelegState()) {
                    GSSCredential delegateCredential = gssContext.getDelegCred();
                    GSSName delegateGSSName = delegateCredential.getName();
                    Principal delegatePrincipal = new KerberosPrincipal(delegateGSSName.toString());
                    subject.getPrincipals().add(delegatePrincipal);
                    subject.getPrivateCredentials().add(delegateCredential);
                    log.info("Got delegated Kerberos principal: " + delegateGSSName);
                }

                /**
                 * A status code 200 status response can also carry a "WWW-Authenticate" response header containing the final leg of an authentication. In this case, the
                 * gssapi-data will be present.
                 */
                if (outToken != null && outToken.length() > 0) {
                    response.setHeader("WWW-Authenticate", "Negotiate " + outToken.getBytes());
                    response.setStatus(HttpServletResponse.SC_OK);
                    log.debug("Returning final authentication data to client to complete context.");
                    log.debug("Negotiation completed.");
                    return true;
                }
            } catch (GSSException gsse) {
                log.error("GSSException:       " + gsse.getMessage());
                log.error("GSSException major: " + gsse.getMajorString());
                log.error("GSSException minor: " + gsse.getMinorString());

                response.addHeader("Client-Warning", gsse.getMessage());
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            }
        } else {
            /**
             * Any returned code other than a success 2xx code represents an authentication error. If a 401 containing a "WWW-Authenticate" header with "Negotiate" and
             * gssapi-data is returned from the server, it is a continuation of the authentication request.
             */
            if (outToken != null && outToken.length() > 0) {
                response.setHeader("WWW-Authenticate", "Negotiate " + outToken.getBytes());
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                log.debug("Additional authentication processing required, returning token.");
                return false;
            } else {
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                log.warn("Kerberos negotiation failed.");
            }
        }

        log.debug("Negotiation completed.");

        return true;
    }

    /**
     * Check whether the Authorization header is attempting the unsupported NTLM mechanism.
     *
     * @param gssapiData Byte array retrieved from the Authorization header.
     * @return true If the header contains an NTLM mechanism request.
     */
    protected boolean isNtlmMechanism(byte[] gssapiData) {
        byte leadingBytes[] = new byte[7];
        System.arraycopy(gssapiData, 0, leadingBytes, 0, 7);
        if (Arrays.equals(leadingBytes, NTLMSSP)) {
            return true;
        }

        return false;
    }
}