org.alfresco.repo.webdav.auth.BaseNTLMAuthenticationFilter.java Source code

Java tutorial

Introduction

Here is the source code for org.alfresco.repo.webdav.auth.BaseNTLMAuthenticationFilter.java

Source

/*
 * #%L
 * Alfresco Remote API
 * %%
 * Copyright (C) 2005 - 2016 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */
package org.alfresco.repo.webdav.auth;

import java.io.IOException;
import java.io.PrintWriter;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.List;
import java.util.Random;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.transaction.Status;
import javax.transaction.UserTransaction;

import net.sf.acegisecurity.BadCredentialsException;

import org.alfresco.jlan.server.auth.PasswordEncryptor;
import org.alfresco.jlan.server.auth.ntlm.NTLM;
import org.alfresco.jlan.server.auth.ntlm.NTLMLogonDetails;
import org.alfresco.jlan.server.auth.ntlm.NTLMMessage;
import org.alfresco.jlan.server.auth.ntlm.NTLMv2Blob;
import org.alfresco.jlan.server.auth.ntlm.TargetInfo;
import org.alfresco.jlan.server.auth.ntlm.Type1NTLMMessage;
import org.alfresco.jlan.server.auth.ntlm.Type2NTLMMessage;
import org.alfresco.jlan.server.auth.ntlm.Type3NTLMMessage;
import org.alfresco.jlan.util.DataPacker;
import org.alfresco.repo.SessionUser;
import org.alfresco.repo.security.authentication.AuthenticationException;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.MD4PasswordEncoder;
import org.alfresco.repo.security.authentication.MD4PasswordEncoderImpl;
import org.alfresco.repo.security.authentication.NTLMMode;
import org.alfresco.repo.security.authentication.ntlm.NLTMAuthenticator;
import org.alfresco.repo.security.authentication.ntlm.NTLMPassthruToken;
import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.repo.web.auth.GuestCredentials;
import org.alfresco.repo.web.auth.NTLMCredentials;
import org.alfresco.repo.web.auth.TicketCredentials;
import org.alfresco.repo.web.auth.UnknownCredentials;
import org.alfresco.repo.web.auth.WebCredentials;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.logging.Log;
import org.springframework.web.util.WebUtils;

/**
 * Base class with common code and initialisation for NTLM authentication filters.
 */
public abstract class BaseNTLMAuthenticationFilter extends BaseSSOAuthenticationFilter {
    // NTLM authentication session object names
    public static final String NTLM_AUTH_SESSION = "_alfNTLMAuthSess";
    public static final String NTLM_AUTH_DETAILS = "_alfNTLMDetails";

    protected static final String WWW_AUTHENTICATE = "WWW-Authenticate";
    protected static final String AUTHORIZATION = "Authorization";
    protected static final String AUTH_NTLM = "NTLM";

    // NTLM flags mask for use with an authentication component that supports MD4 hashed password
    // Enable NTLMv1 and NTLMv2
    private static final int NTLM_FLAGS_NTLM2 = NTLM.Flag56Bit + NTLM.Flag128Bit + NTLM.FlagLanManKey
            + NTLM.FlagNegotiateNTLM + NTLM.FlagNTLM2Key + NTLM.FlagNegotiateUnicode;

    // NTLM flags mask for use with an authentication component that uses passthru auth
    // Enable NTLMv1 only
    private static final int NTLM_FLAGS_NTLM1 = NTLM.Flag56Bit + NTLM.FlagLanManKey + NTLM.FlagNegotiateNTLM
            + NTLM.FlagNegotiateOEM + NTLM.FlagNegotiateUnicode;

    // NTLM flags to send to the client with the allowed logon types
    private int m_ntlmFlags;

    // Password encryptor
    private PasswordEncryptor m_encryptor = new PasswordEncryptor();

    // Random number generator used to generate challenge keys
    private Random m_random = new Random(System.currentTimeMillis());

    // MD4 hash decoder
    private MD4PasswordEncoder m_md4Encoder = new MD4PasswordEncoderImpl();

    // Allow guest access
    private boolean m_allowGuest = false;

    // Allow guest access, map unknown users to the guest account
    private boolean m_mapUnknownUserToGuest = false;

    // Disable NTLMv2 support
    private boolean m_disableNTLMv2 = false;

    // SSO enabled authentication component (required)
    private NLTMAuthenticator nltmAuthenticator;

    /**
     * @param mapUnknownUserToGuest should an unknown user be mapped to guest?
     */
    public void setMapUnknownUserToGuest(boolean mapUnknownUserToGuest) {
        m_mapUnknownUserToGuest = mapUnknownUserToGuest;
    }

    /* (non-Javadoc)
     * @see org.alfresco.repo.webdav.auth.BaseSSOAuthenticationFilter#init()
     */
    @Override
    protected void init() throws ServletException {
        // Call the base SSO filter initialization
        super.init();

        // Check that the authentication component supports the required mode

        if (!(authenticationComponent instanceof NLTMAuthenticator)) {
            throw new ServletException("Authentication component does not support NTLM");
        }
        this.nltmAuthenticator = (NLTMAuthenticator) this.authenticationComponent;
        if (nltmAuthenticator.getNTLMMode() != NTLMMode.MD4_PROVIDER
                && nltmAuthenticator.getNTLMMode() != NTLMMode.PASS_THROUGH) {
            throw new ServletException("Required authentication mode not available");
        }

        // Check if guest access is to be allowed
        m_allowGuest = this.authenticationComponent.guestUserAuthenticationAllowed();

        if (getLogger().isDebugEnabled() && m_allowGuest)
            getLogger().debug("NTLM filter guest access allowed");

        // Check if unknown users should be mapped to guest access
        if (getLogger().isDebugEnabled() && m_mapUnknownUserToGuest)
            getLogger().debug("NTLM filter map unknown users to guest");

        // Set the NTLM flags depending on the authentication component supporting MD4 passwords,
        // or is using passthru auth

        if (nltmAuthenticator.getNTLMMode() == NTLMMode.MD4_PROVIDER && m_disableNTLMv2 == false) {
            // Allow the client to use an NTLMv2 logon

            m_ntlmFlags = NTLM_FLAGS_NTLM2;
        } else {
            // Only allows NTLMv1 type logons as passthru authentication is being used

            m_ntlmFlags = NTLM_FLAGS_NTLM1;
        }
    }

    public boolean authenticateRequest(ServletContext context, HttpServletRequest sreq, HttpServletResponse sresp)
            throws IOException, ServletException {
        // Check if there is an authorization header with an NTLM security blob
        String authHdr = sreq.getHeader(AUTHORIZATION);
        boolean reqAuth = false;

        // Check if an NTLM authorization header was received

        if (authHdr != null) {
            // Check for an NTLM authorization header

            if (authHdr.startsWith(AUTH_NTLM))
                reqAuth = true;
            else if (authHdr.startsWith("Negotiate")) {
                if (getLogger().isDebugEnabled())
                    getLogger().debug("Received 'Negotiate' from client, may be SPNEGO/Kerberos logon");

                // Restart the authentication

                restartLoginChallenge(context, sreq, sresp);
                return false;
            } else if (isFallbackEnabled()) {
                return performFallbackAuthentication(context, sreq, sresp);
            }
        }

        // Check if the user is already authenticated
        SessionUser user = getSessionUser(context, sreq, sresp, true);

        // If the user has been validated and we do not require re-authentication then continue to
        // the next filter
        if (user != null && reqAuth == false) {
            // Filter validate hook
            onValidate(context, sreq, sresp, new TicketCredentials(user.getTicket()));

            if (getLogger().isDebugEnabled())
                getLogger().debug("Authentication not required (user), chaining ...");

            // Chain to the next filter
            return true;
        }

        // Check if the login page is being accessed, do not intercept the login page
        if (hasLoginPage() && sreq.getRequestURI().endsWith(getLoginPage()) == true) {
            if (getLogger().isDebugEnabled())
                getLogger().debug("Login page requested, chaining ...");

            // Chain to the next filter
            return true;
        }

        // Check if the browser is Opera, if so then display the login page as Opera does not
        // support NTLM and displays an error page if a request to use NTLM is sent to it
        String userAgent = sreq.getHeader("user-agent");
        if (userAgent != null && userAgent.indexOf("Opera ") != -1) {
            if (getLogger().isDebugEnabled())
                getLogger().debug("Opera detected, redirecting to login page");

            // If there is no login page configured (WebDAV) then just keep requesting the user details from the client

            if (hasLoginPage())
                redirectToLoginPage(sreq, sresp);
            else
                restartLoginChallenge(context, sreq, sresp);
            return false;
        }

        // Check the authorization header
        if (authHdr == null) {
            // Check for a ticket based logon, if enabled

            if (allowsTicketLogons()) {
                // Check if the request includes an authentication ticket

                if (checkForTicketParameter(context, sreq, sresp)) {

                    // Authentication was bypassed using a ticket parameter
                    return true;
                }
            }

            // DEBUG

            if (getLogger().isDebugEnabled())
                getLogger().debug("New NTLM auth request from " + sreq.getRemoteHost() + " (" + sreq.getRemoteAddr()
                        + ":" + sreq.getRemotePort() + ") SID:" + sreq.getSession().getId());

            // Send back a request for NTLM authentication
            restartLoginChallenge(context, sreq, sresp);
            return false;
        } else {
            HttpSession session = sreq.getSession();
            Object sessionMutex = WebUtils.getSessionMutex(session);
            // Decode the received NTLM blob and validate
            final byte[] ntlmByts = Base64.decodeBase64(authHdr.substring(5).getBytes());
            int ntlmTyp = NTLMMessage.isNTLMType(ntlmByts);
            if (ntlmTyp == NTLM.Type1) {
                // Process the type 1 NTLM message
                Type1NTLMMessage type1Msg = new Type1NTLMMessage(ntlmByts);
                synchronized (sessionMutex) {
                    processType1(type1Msg, sreq, sresp);
                }
                return false;
            } else if (ntlmTyp == NTLM.Type3) {
                // Process the type 3 NTLM message
                Type3NTLMMessage type3Msg = new Type3NTLMMessage(ntlmByts);
                synchronized (sessionMutex) {
                    return processType3(type3Msg, context, sreq, sresp);
                }
            } else {
                if (getLogger().isDebugEnabled())
                    getLogger().debug("NTLM blob not handled, redirecting to login page.");

                if (hasLoginPage())
                    redirectToLoginPage(sreq, sresp);
                else
                    restartLoginChallenge(context, sreq, sresp);
                return false;
            }
        }
    }

    /**
     * Process a type 1 NTLM message
     * 
     * @param type1Msg Type1NTLMMessage
     * @param req HttpServletRequest
     * @param res HttpServletResponse
     * @exception IOException
     */
    protected void processType1(Type1NTLMMessage type1Msg, HttpServletRequest req, HttpServletResponse res)
            throws IOException {
        if (getLogger().isDebugEnabled())
            getLogger().debug("Received type1 " + type1Msg);

        // Get the existing NTLM details
        NTLMLogonDetails ntlmDetails = null;
        HttpSession session = req.getSession();
        ntlmDetails = (NTLMLogonDetails) session.getAttribute(NTLM_AUTH_DETAILS);

        // Check if cached logon details are available
        if (ntlmDetails != null && ntlmDetails.hasType2Message()
                && ((nltmAuthenticator.getNTLMMode() == NTLMMode.PASS_THROUGH
                        && ntlmDetails.hasAuthenticationToken()) || !ntlmDetails.hasAuthenticationToken())) {
            // Get the authentication server type2 response
            Type2NTLMMessage cachedType2 = ntlmDetails.getType2Message();

            byte[] type2Bytes = cachedType2.getBytes();
            String ntlmBlob = "NTLM " + new String(Base64.encodeBase64(type2Bytes));

            if (getLogger().isDebugEnabled())
                getLogger().debug("Sending cached NTLM type2 to client - " + cachedType2);

            // Send back a request for NTLM authentication
            res.setHeader(WWW_AUTHENTICATE, ntlmBlob);
            res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            res.flushBuffer();
        } else {
            // Clear any cached logon details
            session.removeAttribute(NTLM_AUTH_DETAILS);

            // Set the 8 byte challenge for the new logon request
            byte[] challenge = null;
            NTLMPassthruToken authToken = null;

            if (nltmAuthenticator.getNTLMMode() == NTLMMode.MD4_PROVIDER) {
                // Generate a random 8 byte challenge

                challenge = new byte[8];
                DataPacker.putIntelLong(m_random.nextLong(), challenge, 0);
            } else {
                // Get the client domain
                String domain = type1Msg.getDomain();
                if (domain == null || domain.length() == 0) {
                    domain = mapClientAddressToDomain(req.getRemoteAddr());
                }

                if (getLogger().isDebugEnabled())
                    getLogger().debug("Client domain " + domain);

                // Create an authentication token for the new logon
                authToken = new NTLMPassthruToken(domain);

                // Run the first stage of the passthru authentication to get the challenge
                nltmAuthenticator.authenticate(authToken);

                // Get the challenge from the token
                if (authToken.getChallenge() != null) {
                    challenge = authToken.getChallenge().getBytes();
                }
            }

            // Get the flags from the client request and mask out unsupported features
            int ntlmFlags = type1Msg.getFlags() & m_ntlmFlags;

            // Build a type2 message to send back to the client, containing the challenge
            List<TargetInfo> tList = new ArrayList<TargetInfo>();
            String srvName = getServerName();
            tList.add(new TargetInfo(NTLM.TargetServer, srvName));

            Type2NTLMMessage type2Msg = new Type2NTLMMessage();
            type2Msg.buildType2(ntlmFlags, srvName, challenge, null, tList);

            // Store the NTLM logon details, cache the type2 message, and token if using passthru
            ntlmDetails = new NTLMLogonDetails();
            ntlmDetails.setType2Message(type2Msg);
            ntlmDetails.setAuthenticationToken(authToken);

            session.setAttribute(NTLM_AUTH_DETAILS, ntlmDetails);

            if (getLogger().isDebugEnabled())
                getLogger().debug("Sending NTLM type2 to client - " + type2Msg);

            // Send back a request for NTLM authentication
            byte[] type2Bytes = type2Msg.getBytes();
            String ntlmBlob = "NTLM " + new String(Base64.encodeBase64(type2Bytes));

            res.setHeader(WWW_AUTHENTICATE, ntlmBlob);
            res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            res.flushBuffer();
        }
    }

    /**
     * Process a type 3 NTLM message
     * 
     * @param type3Msg Type3NTLMMessage
     * @param req HttpServletRequest
     * @param res HttpServletResponse
     *
     * @exception IOException
     * @exception ServletException
     */
    protected boolean processType3(Type3NTLMMessage type3Msg, ServletContext context, HttpServletRequest req,
            HttpServletResponse res) throws IOException, ServletException {
        Log logger = getLogger();

        if (logger.isDebugEnabled())
            logger.debug("Received type3 " + type3Msg);

        // Get the existing NTLM details
        NTLMLogonDetails ntlmDetails = null;
        SessionUser user = null;

        user = getSessionUser(context, req, res, true);
        HttpSession session = req.getSession();
        ntlmDetails = (NTLMLogonDetails) session.getAttribute(NTLM_AUTH_DETAILS);

        // Get the NTLM logon details
        String userName = type3Msg.getUserName();
        String workstation = type3Msg.getWorkstation();
        String domain = type3Msg.getDomain();

        // ALF-10997 fix, normalize the userName
        //the system runAs is acceptable because we are resolving a username i.e. it's a system-wide operation that does not need permission checks

        final String userName_f = userName;
        String normalized = transactionService.getRetryingTransactionHelper()
                .doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<String>() {
                    public String execute() throws Throwable {
                        return AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork<String>() {
                            public String doWork() throws Exception {
                                String normalized = personService.getUserIdentifier(userName_f);
                                return normalized;
                            }
                        }, AuthenticationUtil.SYSTEM_USER_NAME);
                    }
                }, true);

        if (normalized != null) {
            userName = normalized;
        }

        boolean authenticated = false;

        // Check if we are using cached details for the authentication
        if (user != null && ntlmDetails != null && ntlmDetails.hasNTLMHashedPassword()) {
            // Check if the received NTLM hashed password matches the cached password
            byte[] ntlmPwd = type3Msg.getNTLMHash();
            byte[] cachedPwd = ntlmDetails.getNTLMHashedPassword();

            if (ntlmPwd != null) {
                authenticated = Arrays.equals(cachedPwd, ntlmPwd);
            }

            if (logger.isDebugEnabled())
                logger.debug("Using cached NTLM hash, authenticated = " + authenticated);

            onValidate(context, req, res, new NTLMCredentials(userName, ntlmPwd));

            // Allow the user to access the requested page
            return true;
        } else {
            WebCredentials credentials;
            // Check if we are using local MD4 password hashes or passthru authentication
            if (nltmAuthenticator.getNTLMMode() == NTLMMode.MD4_PROVIDER) {
                // Check if guest logons are allowed and this is a guest logon
                if (m_allowGuest && userName.equalsIgnoreCase(authenticationComponent.getGuestUserName())) {
                    credentials = new GuestCredentials();
                    // Indicate that the user has been authenticated
                    authenticated = true;

                    if (getLogger().isDebugEnabled())
                        getLogger().debug("Guest logon");
                } else {
                    // Get the stored MD4 hashed password for the user, or null if the user does not exist
                    String md4hash = getMD4Hash(userName);

                    if (md4hash != null) {
                        authenticated = validateLocalHashedPassword(type3Msg, ntlmDetails, authenticated, md4hash);
                        credentials = new NTLMCredentials(ntlmDetails.getUserName(),
                                ntlmDetails.getNTLMHashedPassword());
                    } else {
                        // Check if unknown users should be logged on as guest
                        if (m_mapUnknownUserToGuest) {
                            // Reset the user name to be the guest user
                            userName = authenticationComponent.getGuestUserName();
                            authenticated = true;
                            credentials = new GuestCredentials();

                            if (logger.isDebugEnabled())
                                logger.debug("User " + userName + " logged on as guest, no Alfresco account");
                        } else {
                            if (logger.isDebugEnabled())
                                logger.debug("User " + userName + " does not have Alfresco account");

                            // Bypass NTLM authentication and display the logon screen,
                            // as user account does not exist in Alfresco
                            credentials = new UnknownCredentials();
                            authenticated = false;
                        }
                    }
                }
            } else {
                credentials = new NTLMCredentials(type3Msg.getUserName(), type3Msg.getNTLMHash());
                //  Determine if the client sent us NTLMv1 or NTLMv2
                if (type3Msg.hasFlag(NTLM.Flag128Bit) && type3Msg.hasFlag(NTLM.FlagNTLM2Key)
                        || (type3Msg.getNTLMHash() != null && type3Msg.getNTLMHash().length > 24)) {
                    // Cannot accept NTLMv2 if we are using passthru auth
                    if (logger.isErrorEnabled())
                        logger.error("Client " + workstation
                                + " using NTLMv2 logon, not valid with passthru authentication");
                } else {
                    if (ntlmDetails == null) {
                        if (logger.isWarnEnabled())
                            logger.warn(
                                    "Authentication failed: NTLM details can not be retrieved from session. Client must support cookies.");
                        restartLoginChallenge(context, req, res);
                        return false;
                    }
                    // Passthru mode, send the hashed password details to the passthru authentication server
                    NTLMPassthruToken authToken = (NTLMPassthruToken) ntlmDetails.getAuthenticationToken();
                    authToken.setUserAndPassword(type3Msg.getUserName(), type3Msg.getNTLMHash(),
                            PasswordEncryptor.NTLM1);
                    try {
                        // Run the second stage of the passthru authentication
                        nltmAuthenticator.authenticate(authToken);
                        authenticated = true;

                        // Check if the user has been logged on as guest
                        if (authToken.isGuestLogon()) {
                            userName = authenticationComponent.getGuestUserName();
                        }

                        // Set the authentication context
                        authenticationComponent.setCurrentUser(userName);
                    } catch (BadCredentialsException ex) {
                        if (logger.isDebugEnabled())
                            logger.debug("Authentication failed, " + ex.getMessage());
                    } catch (AuthenticationException ex) {
                        if (logger.isDebugEnabled())
                            logger.debug("Authentication failed, " + ex.getMessage());
                    } finally {
                        // Clear the authentication token from the NTLM details
                        ntlmDetails.setAuthenticationToken(null);
                    }
                }
            }

            // Check if the user has been authenticated, if so then setup the user environment
            if (authenticated == true) {
                boolean userInit = false;
                if (user == null) {
                    try {
                        user = createUserEnvironment(session, userName);
                        userInit = true;
                    } catch (AuthenticationException ex) {
                        if (logger.isDebugEnabled())
                            logger.debug("Failed to validate user " + userName, ex);

                        onValidateFailed(context, req, res, session, credentials);
                        return false;
                    }
                }

                onValidate(context, req, res, credentials);

                // Update the NTLM logon details in the session
                String srvName = getServerName();
                if (ntlmDetails == null) {
                    // No cached NTLM details
                    ntlmDetails = new NTLMLogonDetails(userName, workstation, domain, false, srvName);
                    ntlmDetails.setNTLMHashedPassword(type3Msg.getNTLMHash());
                    session.setAttribute(NTLM_AUTH_DETAILS, ntlmDetails);

                    if (logger.isDebugEnabled())
                        logger.debug("No cached NTLM details, created");
                } else {
                    // Update the cached NTLM details
                    ntlmDetails.setDetails(userName, workstation, domain, false, srvName);
                    ntlmDetails.setNTLMHashedPassword(type3Msg.getNTLMHash());

                    if (logger.isDebugEnabled())
                        logger.debug("Updated cached NTLM details");
                }

                if (logger.isDebugEnabled())
                    logger.debug("User logged on via NTLM, " + ntlmDetails);

                if (onLoginComplete(context, req, res, userInit)) {
                    // Allow the user to access the requested page
                    return true;
                }
            } else {
                restartLoginChallenge(context, req, res);
            }
        }
        return false;
    }

    /**
     * Validate the MD4 hash against local password
     * 
     * @param type3Msg Type3NTLMMessage
     * @param ntlmDetails NTLMLogonDetails
     * @param authenticated boolean
     * @param md4hash String
     * 
     * @return true if password hash is valid, false otherwise
     */
    protected boolean validateLocalHashedPassword(Type3NTLMMessage type3Msg, NTLMLogonDetails ntlmDetails,
            boolean authenticated, String md4hash) {
        // Make sure we have hte cached NTLM details, including the type2 message with the server challenge

        if (ntlmDetails == null || ntlmDetails.getType2Message() == null) {
            // DEBUG

            if (getLogger().isDebugEnabled())
                getLogger().debug("No cached Type2, ntlmDetails=" + ntlmDetails);

            // Not authenticated

            return false;
        }

        //  Determine if the client sent us NTLMv1 or NTLMv2
        if (type3Msg.hasFlag(NTLM.FlagNTLM2Key)) {
            //  Determine if the client sent us an NTLMv2 blob or an NTLMv2 session key
            if (type3Msg.getNTLMHashLength() > 24) {
                //  Looks like an NTLMv2 blob
                authenticated = checkNTLMv2(md4hash, ntlmDetails.getChallengeKey(), type3Msg);

                if (getLogger().isDebugEnabled())
                    getLogger().debug((authenticated ? "Logged on" : "Logon failed") + " using NTLMSSP/NTLMv2");

                // If the NTlmv2 autentication failed then check if the client has sent an NTLMv1 hash

                if (authenticated == false && type3Msg.hasFlag(NTLM.Flag56Bit)
                        && type3Msg.getLMHashLength() == 24) {
                    // Check the LM hash field

                    authenticated = checkNTLMv1(md4hash, ntlmDetails.getChallengeKey(), type3Msg, true);

                    // DEBUG

                    if (getLogger().isDebugEnabled())
                        getLogger().debug((authenticated ? "Logged on" : "Logon failed")
                                + " using NTLMSSP/NTLMv1 (via fallback)");
                }
            } else {
                //  Looks like an NTLMv2 session key
                authenticated = checkNTLMv2SessionKey(md4hash, ntlmDetails.getChallengeKey(), type3Msg);

                if (getLogger().isDebugEnabled())
                    getLogger()
                            .debug((authenticated ? "Logged on" : "Logon failed") + " using NTLMSSP/NTLMv2SessKey");
            }
        } else {
            //  Looks like an NTLMv1 blob
            authenticated = checkNTLMv1(md4hash, ntlmDetails.getChallengeKey(), type3Msg, false);

            if (getLogger().isDebugEnabled())
                getLogger().debug((authenticated ? "Logged on" : "Logon failed") + " using NTLMSSP/NTLMv1");
        }
        return authenticated;
    }

    /**
     * Perform an NTLMv1 hashed password check
     * 
     * @param md4hash String
     * @param challenge byte[]
     * @param type3Msg Type3NTLMMessage
     * @param checkLMHash boolean
     * @return boolean
     */
    protected final boolean checkNTLMv1(String md4hash, byte[] challenge, Type3NTLMMessage type3Msg,
            boolean checkLMHash) {
        if (getLogger().isDebugEnabled())
            getLogger().debug(("Perform an NTLMv1 hashed password check."));

        // Generate the local encrypted password using the challenge that was sent to the client
        byte[] p21 = new byte[21];
        byte[] md4byts = m_md4Encoder.decodeHash(md4hash);
        System.arraycopy(md4byts, 0, p21, 0, 16);

        // Generate the local hash of the password using the same challenge
        byte[] localHash = null;

        try {
            localHash = m_encryptor.doNTLM1Encryption(p21, challenge);
        } catch (NoSuchAlgorithmException ex) {
        }

        // Validate the password
        byte[] clientHash = checkLMHash ? type3Msg.getLMHash() : type3Msg.getNTLMHash();

        if (clientHash != null && localHash != null && clientHash.length == localHash.length) {
            int i = 0;

            while (i < clientHash.length && clientHash[i] == localHash[i]) {
                i++;
            }

            if (i == clientHash.length) {
                if (getLogger().isDebugEnabled())
                    getLogger().debug(("Hashed passwords match."));
                return true;
            }
        }

        if (getLogger().isDebugEnabled())
            getLogger().debug(("Hashed passwords do not match."));
        return false;
    }

    /**
     * Perform an NTLMv2 check
     * 
     * @param md4hash String
     * @param challenge byte[]
     * @param type3Msg Type3NTLMMessage
     * @return boolean
     */
    protected final boolean checkNTLMv2(String md4hash, byte[] challenge, Type3NTLMMessage type3Msg) {
        if (getLogger().isDebugEnabled())
            getLogger().debug(("Perform an NTLMv2 check."));
        boolean ntlmv2OK = false;
        boolean lmv2OK = false;

        try {
            // Generate the v2 hash using the challenge that was sent to the client
            byte[] v2hash = m_encryptor.doNTLM2Encryption(m_md4Encoder.decodeHash(md4hash), type3Msg.getUserName(),
                    type3Msg.getDomain());

            // Get the NTLMv2 blob sent by the client and the challenge that was sent by the server
            NTLMv2Blob v2blob = new NTLMv2Blob(type3Msg.getNTLMHash());

            // Calculate the HMAC of the received blob and compare
            byte[] srvHmac = v2blob.calculateHMAC(challenge, v2hash);
            byte[] clientHmac = v2blob.getHMAC();

            if (clientHmac != null && srvHmac != null && clientHmac.length == srvHmac.length) {
                int i = 0;

                while (i < clientHmac.length && clientHmac[i] == srvHmac[i]) {
                    i++;
                }

                if (i == clientHmac.length) {
                    if (getLogger().isDebugEnabled())
                        getLogger().debug(("HMAC matches the client, user authenticated."));
                    ntlmv2OK = true;
                }
            }

            // NTLMv2 check failed, try the LMv2 value

            if (ntlmv2OK == false) {
                byte[] lmv2 = type3Msg.getLMHash();
                byte[] clChallenge = v2blob.getClientChallenge();

                if (lmv2 != null && lmv2.length == 24 && clChallenge != null && clChallenge.length == 8) {
                    // Check that the LM hash contains the client challenge as the last 8 bytes

                    int i = 0;

                    while (i < clChallenge.length && lmv2[i + 16] == clChallenge[i])
                        i++;

                    if (i == clChallenge.length) {
                        // Calculate the LMv2 value

                        byte[] lmv2Hmac = v2blob.calculateLMv2HMAC(v2hash, challenge, clChallenge);

                        // Check if the LMv2 HMAC matches

                        i = 0;

                        while (i < lmv2Hmac.length && lmv2[i] == lmv2Hmac[i])
                            i++;

                        if (i == lmv2Hmac.length) {
                            if (getLogger().isDebugEnabled())
                                getLogger().debug(("LMv2 HMAC matches the client, user authenticated."));

                            //return true;
                            lmv2OK = true;
                        }

                    }
                }
            }
        } catch (Exception ex) {
            if (getLogger().isDebugEnabled())
                getLogger().debug(ex);
        }

        // Check if either of the NTLMv2 checks passed

        if (ntlmv2OK || lmv2OK)
            return true;
        return false;
    }

    /**
     * Perform an NTLMv2 session key check
     * 
     * @param md4hash String
     * @param challenge byte[]
     * @param type3Msg Type3NTLMMessage
     * @return boolean
     */
    protected final boolean checkNTLMv2SessionKey(String md4hash, byte[] challenge, Type3NTLMMessage type3Msg) {
        if (getLogger().isDebugEnabled())
            getLogger().debug(("Perform an NTLMv2 session key check."));
        // Create the value to be encrypted by appending the server challenge and client challenge
        // and applying an MD5 digest
        byte[] nonce = new byte[16];
        System.arraycopy(challenge, 0, nonce, 0, 8);
        System.arraycopy(type3Msg.getLMHash(), 0, nonce, 8, 8);

        MessageDigest md5 = null;
        byte[] v2challenge = new byte[8];

        try {
            //  Create the MD5 digest
            md5 = MessageDigest.getInstance("MD5");

            //  Apply the MD5 digest to the nonce
            md5.update(nonce);
            byte[] md5nonce = md5.digest();

            //  We only want the first 8 bytes
            System.arraycopy(md5nonce, 0, v2challenge, 0, 8);
        } catch (NoSuchAlgorithmException ex) {
            // Log the error
            getLogger().error(ex);
        }

        // Generate the local encrypted password using the MD5 generated challenge
        byte[] p21 = new byte[21];
        byte[] md4byts = m_md4Encoder.decodeHash(md4hash);
        System.arraycopy(md4byts, 0, p21, 0, 16);

        // Generate the local hash of the password
        byte[] localHash = null;

        try {
            localHash = m_encryptor.doNTLM1Encryption(p21, v2challenge);
        } catch (NoSuchAlgorithmException ex) {
            // Log the error
            getLogger().error(ex);
        }

        // Validate the password
        byte[] clientHash = type3Msg.getNTLMHash();

        if (clientHash != null && localHash != null && clientHash.length == localHash.length) {
            int i = 0;

            while (i < clientHash.length && clientHash[i] == localHash[i]) {
                i++;
            }

            if (i == clientHash.length) {
                if (getLogger().isDebugEnabled())
                    getLogger().debug(("Hashed password check successful."));
                return true;
            }
        }
        if (getLogger().isDebugEnabled())
            getLogger().debug(("Password check failed."));
        return false;
    }

    /**
     * Get the stored MD4 hashed password for the user, or null if the user does not exist
     * 
     * @param userName String
     *
     * @return MD4 hash or null
     */
    protected String getMD4Hash(String userName) {
        String md4hash = null;

        // Wrap the auth component calls in a transaction
        UserTransaction tx = transactionService.getUserTransaction();
        try {
            tx.begin();

            // Get the stored MD4 hashed password for the user, or null if the user does not exist
            md4hash = nltmAuthenticator.getMD4HashedPassword(userName);
        } catch (Throwable ex) {
            if (getLogger().isDebugEnabled())
                getLogger().debug(ex);
        } finally {
            // Rollback/commit the transaction if still valid
            if (tx != null) {
                try {
                    // Commit or rollback the transaction
                    if (tx.getStatus() == Status.STATUS_MARKED_ROLLBACK
                            || tx.getStatus() == Status.STATUS_ROLLEDBACK
                            || tx.getStatus() == Status.STATUS_ROLLING_BACK) {
                        // Transaction is marked for rollback
                        tx.rollback();
                    } else {
                        // Commit the transaction
                        tx.commit();
                    }
                } catch (Throwable ex) {
                    if (getLogger().isDebugEnabled())
                        getLogger().debug(ex);
                }
            }
        }

        return md4hash;
    }

    /**
     * Restart the NTLM logon process
     * 
     * @param context ServletContext
     * @param req HttpServletRequest
     * @param res SessHttpServletResponse
     * @throws IOException
     */
    public void restartLoginChallenge(ServletContext context, HttpServletRequest req, HttpServletResponse res)
            throws IOException {
        if (getLogger().isDebugEnabled())
            getLogger().debug("restartLoginChallenge...");

        // Remove any existing session and NTLM details from the session
        HttpSession session = req.getSession(false);
        if (session != null) {
            clearSession(session);
        }

        // check for "chrome" since Chrome user-agent contains a Safari version
        String userAgent = req.getHeader("user-agent");
        if (userAgent != null && userAgent.indexOf("Safari") != -1 && userAgent.indexOf("Chrome") == -1) {
            final PrintWriter out = res.getWriter();
            out.println("<html><head></head>");
            out.println("<body><p>Login authentication failed. Please close and re-open Safari to try again.</p>");
            out.println("</body></html>");
            out.close();
        } else {
            // Force the logon to start again
            res.setHeader(WWW_AUTHENTICATE, AUTH_NTLM);
            res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            writeLoginPageLink(context, req, res);
        }

        if (isFallbackEnabled()) {
            includeFallbackAuth(context, req, res);
        }

        res.flushBuffer();
    }

    /**
     * Removes all attributes stored in session
     * 
     * @param session Session
     */
    @SuppressWarnings("unchecked")
    private void clearSession(HttpSession session) {
        Enumeration<String> names = (Enumeration<String>) session.getAttributeNames();
        while (names.hasMoreElements()) {
            session.removeAttribute(names.nextElement());
        }
    }

    /**
     * Disable NTLMv2 support, must be called from the implementation constructor
     */
    protected final void disableNTLMv2() {
        m_disableNTLMv2 = true;
    }
}