diff --git a/source/java/org/alfresco/repo/SessionUser.java b/source/java/org/alfresco/repo/SessionUser.java new file mode 100644 index 0000000000..80fd85d9e1 --- /dev/null +++ b/source/java/org/alfresco/repo/SessionUser.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2005-2007 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program 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 General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing + */ +package org.alfresco.repo; + +import java.io.Serializable; + +/** + * Contract implemented by any object that represents an Alfresco "user" that + * can be persisted in an HTTP Session. + * + * @author Kevin Roast + */ +public interface SessionUser extends Serializable +{ + /** + * Return the user name + * + * @return user name + */ + String getUserName(); + + /** + * Return the ticket + * + * @return ticket + */ + String getTicket(); +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/web/scripts/bean/LoginTicket.java b/source/java/org/alfresco/repo/web/scripts/bean/LoginTicket.java index 3104bc51ed..e48ea3ae0b 100644 --- a/source/java/org/alfresco/repo/web/scripts/bean/LoginTicket.java +++ b/source/java/org/alfresco/repo/web/scripts/bean/LoginTicket.java @@ -71,9 +71,9 @@ public class LoginTicket extends DeclarativeWebScript } // construct model for ticket - Map model = new HashMap(7, 1.0f); + Map model = new HashMap(1, 1.0f); model.put("ticket", ticket); - + try { String ticketUser = ticketComponent.validateTicket(ticket); @@ -86,7 +86,7 @@ public class LoginTicket extends DeclarativeWebScript status.setMessage("Ticket not found"); } } - catch(AuthenticationException e) + catch (AuthenticationException e) { status.setRedirect(true); status.setCode(HttpServletResponse.SC_NOT_FOUND); diff --git a/source/java/org/alfresco/repo/web/scripts/bean/LoginTicketDelete.java b/source/java/org/alfresco/repo/web/scripts/bean/LoginTicketDelete.java index 503dd6e854..b8e36b0f5e 100644 --- a/source/java/org/alfresco/repo/web/scripts/bean/LoginTicketDelete.java +++ b/source/java/org/alfresco/repo/web/scripts/bean/LoginTicketDelete.java @@ -81,7 +81,7 @@ public class LoginTicketDelete extends DeclarativeWebScript } // construct model for ticket - Map model = new HashMap(7, 1.0f); + Map model = new HashMap(1, 1.0f); model.put("ticket", ticket); try diff --git a/source/java/org/alfresco/repo/webdav/auth/BaseNTLMAuthenticationFilter.java b/source/java/org/alfresco/repo/webdav/auth/BaseNTLMAuthenticationFilter.java new file mode 100644 index 0000000000..2446a43fa2 --- /dev/null +++ b/source/java/org/alfresco/repo/webdav/auth/BaseNTLMAuthenticationFilter.java @@ -0,0 +1,997 @@ +/* + * Copyright (C) 2005-2007 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program 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 General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing + */ +package org.alfresco.repo.webdav.auth; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +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.filesys.ServerConfigurationBean; +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.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.server.auth.passthru.DomainMapping; +import org.alfresco.jlan.server.config.SecurityConfigSection; +import org.alfresco.jlan.util.DataPacker; +import org.alfresco.jlan.util.IPAddress; +import org.alfresco.repo.SessionUser; +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.repo.security.authentication.AuthenticationException; +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.NTLMPassthruToken; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.transaction.TransactionService; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.logging.Log; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.WebApplicationContextUtils; + +/** + * Base class with common code and initialisation for NTLM authentication filters. + */ +public abstract class BaseNTLMAuthenticationFilter implements Filter +{ + // 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 + private static final int NTLM_FLAGS_MD4 = 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 + private static final int NTLM_FLAGS_PASSTHRU = 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; + + // Servlet context, required to get authentication service + protected ServletContext m_context; + + // File server configuration + private ServerConfigurationBean m_srvConfig; + + // Security configuration section, for domain mappings + private SecurityConfigSection m_secConfig; + + // Various services required by NTLM authenticator + protected AuthenticationService m_authService; + protected AuthenticationComponent m_authComponent; + protected PersonService m_personService; + protected NodeService m_nodeService; + protected TransactionService m_transactionService; + + // 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(); + + // Local server name, from either the file servers config or DNS host name + protected String m_srvName; + + // Allow guest access + private boolean m_allowGuest = false; + + // Allow guest access, map unknown users to the guest account + private boolean m_mapUnknownUserToGuest = false; + + + /** + * Initialize the filter + * + * @param args FilterConfig + * + * @exception ServletException + */ + public void init(FilterConfig args) throws ServletException + { + // Save the servlet context, needed to get hold of the authentication service + m_context = args.getServletContext(); + + // Setup the authentication context + WebApplicationContext ctx = WebApplicationContextUtils.getRequiredWebApplicationContext(m_context); + + ServiceRegistry serviceRegistry = (ServiceRegistry) ctx.getBean(ServiceRegistry.SERVICE_REGISTRY); + m_nodeService = serviceRegistry.getNodeService(); + m_transactionService = serviceRegistry.getTransactionService(); + m_authService = serviceRegistry.getAuthenticationService(); + + m_authComponent = (AuthenticationComponent) ctx.getBean("AuthenticationComponent"); + m_personService = (PersonService) ctx.getBean("personService"); + + m_srvConfig = (ServerConfigurationBean) ctx.getBean(ServerConfigurationBean.SERVER_CONFIGURATION); + + // Check that the authentication component supports the required mode + if (m_authComponent.getNTLMMode() != NTLMMode.MD4_PROVIDER && + m_authComponent.getNTLMMode() != NTLMMode.PASS_THROUGH) + { + throw new ServletException("Required authentication mode not available"); + } + + // Get the local server name, try the file server config first + if (m_srvConfig != null) + { + m_srvName = m_srvConfig.getServerName(); + + if (m_srvName != null) + { + try + { + InetAddress resolved = InetAddress.getByName(m_srvName); + if (resolved == null) + { + // failed to resolve the configured name + m_srvName = m_srvConfig.getLocalServerName(true); + } + } + catch (UnknownHostException ex) + { + if (getLogger().isErrorEnabled()) + getLogger().error("NTLM filter, error getting resolving host name", ex); + } + } + else + { + m_srvName = m_srvConfig.getLocalServerName(true); + } + } + else + { + // Get the host name + try + { + // Get the local host name + m_srvName = InetAddress.getLocalHost().getHostName(); + + // Strip any domain name + int pos = m_srvName.indexOf("."); + if (pos != -1) + { + m_srvName = m_srvName.substring(0, pos - 1); + } + } + catch (UnknownHostException ex) + { + if (getLogger().isErrorEnabled()) + getLogger().error("NTLM filter, error getting local host name", ex); + } + } + + // Check if the server name is valid + if (m_srvName == null || m_srvName.length() == 0) + { + throw new ServletException("Failed to get local server name"); + } + + // Check if guest access is to be allowed + String guestAccess = args.getInitParameter("AllowGuest"); + if (guestAccess != null) + { + m_allowGuest = Boolean.parseBoolean(guestAccess); + + if (getLogger().isDebugEnabled() && m_allowGuest) + getLogger().debug("NTLM filter guest access allowed"); + } + + // Check if unknown users should be mapped to guest access + String mapUnknownToGuest = args.getInitParameter("MapUnknownUserToGuest"); + if (mapUnknownToGuest != null) + { + m_mapUnknownUserToGuest = Boolean.parseBoolean(mapUnknownToGuest); + + 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 (m_authComponent.getNTLMMode() == NTLMMode.MD4_PROVIDER) + { + // Allow the client to use an NTLMv2 logon + m_ntlmFlags = NTLM_FLAGS_MD4; + } + else + { + // Only allows NTLMv1 type logons as passthru authentication is being used + m_ntlmFlags = NTLM_FLAGS_PASSTHRU; + } + } + + /** + * Delete the servlet filter + */ + public void destroy() + { + } + + /** + * Process a type 1 NTLM message + * + * @param type1Msg Type1NTLMMessage + * @param req HttpServletRequest + * @param res HttpServletResponse + * @param session HttpSession + * @exception IOException + */ + protected void processType1(Type1NTLMMessage type1Msg, HttpServletRequest req, + HttpServletResponse res, HttpSession session) throws IOException + { + Log logger = getLogger(); + + if (logger.isDebugEnabled()) + logger.debug("Received type1 " + type1Msg); + + // Get the existing NTLM details + NTLMLogonDetails ntlmDetails = null; + + if (session != null) + { + ntlmDetails = (NTLMLogonDetails)session.getAttribute(NTLM_AUTH_DETAILS); + } + + // Check if cached logon details are available + if (ntlmDetails != null && ntlmDetails.hasType2Message() && + ntlmDetails.hasNTLMHashedPassword() && 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 (logger.isDebugEnabled()) + logger.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 (m_authComponent.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 (logger.isDebugEnabled()) + logger.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 + m_authComponent.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 tList = new ArrayList(); + tList.add(new TargetInfo(NTLM.TargetServer, m_srvName)); + + Type2NTLMMessage type2Msg = new Type2NTLMMessage(); + type2Msg.buildType2(ntlmFlags, m_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 (logger.isDebugEnabled()) + logger.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 + * @param session HttpSession + * @param chain FilterChain + * + * @exception IOException + * @exception ServletException + */ + protected void processType3(Type3NTLMMessage type3Msg, HttpServletRequest req, HttpServletResponse res, + HttpSession session, FilterChain chain) 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; + + if (session != null) + { + ntlmDetails = (NTLMLogonDetails)session.getAttribute(NTLM_AUTH_DETAILS); + user = getSessionUser(session); + } + + // Get the NTLM logon details + String userName = type3Msg.getUserName(); + String workstation = type3Msg.getWorkstation(); + String domain = type3Msg.getDomain(); + + 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) + { + if (ntlmPwd.length == cachedPwd.length) + { + authenticated = true; + for (int i = 0; i < ntlmPwd.length; i++) + { + if (ntlmPwd[i] != cachedPwd[i]) + authenticated = false; + } + } + } + + if (logger.isDebugEnabled()) + logger.debug("Using cached NTLM hash, authenticated = " + authenticated); + + try + { + if (logger.isDebugEnabled()) + logger.debug("User " + user.getUserName() + " validate ticket"); + + // Validate the user ticket + m_authService.validate(user.getTicket()); + + onValidate(req, session); + } + catch (AuthenticationException ex) + { + if (logger.isErrorEnabled()) + logger.error("Failed to validate user " + user.getUserName(), ex); + + onValidateFailed(req, res, session); + return; + } + + // Allow the user to access the requested page + chain.doFilter(req, res); + return; + } + else + { + // Check if we are using local MD4 password hashes or passthru authentication + if (m_authComponent.getNTLMMode() == NTLMMode.MD4_PROVIDER) + { + // Check if guest logons are allowed and this is a guest logon + if (m_allowGuest && userName.equalsIgnoreCase(m_authComponent.getGuestUserName())) + { + // Indicate that the user has been authenticated + authenticated = true; + + if (logger.isDebugEnabled()) + logger.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); + } + else + { + // Check if unknown users should be logged on as guest + if (m_mapUnknownUserToGuest) + { + // Reset the user name to be the guest user + userName = m_authComponent.getGuestUserName(); + authenticated = true; + + 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 + authenticated = false; + } + } + } + } + else + { + // 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 + { + // 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 + m_authComponent.authenticate(authToken); + authenticated = true; + + // Check if the user has been logged on as guest + if (authToken.isGuestLogon()) + { + userName = m_authComponent.getGuestUserName(); + } + + // Set the authentication context + m_authComponent.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) + { + if (user == null) + { + user = createUserEnvironment(session, userName); + } + else + { + // user already exists - revalidate ticket to authenticate the current user thread + try + { + m_authService.validate(user.getTicket()); + } + catch (AuthenticationException ex) + { + if (logger.isErrorEnabled()) + logger.error("Failed to validate user " + user.getUserName(), ex); + + onValidateFailed(req, res, session); + return; + } + } + + onValidate(req, session); + + // Update the NTLM logon details in the session + if (ntlmDetails == null) + { + // No cached NTLM details + ntlmDetails = new NTLMLogonDetails( userName, workstation, domain, false, m_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, m_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(req, res)) + { + // Allow the user to access the requested page + chain.doFilter(req, res); + } + } + else + { + restartLoginChallenge(res, session); + } + } + } + + /** + * Callback to get the specific impl of the Session User for a filter + * + * @return User from the session + */ + protected abstract SessionUser getSessionUser(HttpSession session); + + /** + * Callback to create the User environment as appropriate for a filter impl + * + * @param session + * @param userName + * + * @return SessionUser impl + * + * @throws IOException + * @throws ServletException + */ + protected abstract SessionUser createUserEnvironment(HttpSession session, String userName) + throws IOException, ServletException; + + /** + * Callback executed on successful ticket validation during Type3 Message processing + */ + protected abstract void onValidate(HttpServletRequest req, HttpSession session); + + /** + * Callback executed on failed authentication of a user ticket during Type3 Message processing + */ + protected abstract void onValidateFailed(HttpServletRequest req, HttpServletResponse res, HttpSession session) + throws IOException; + + /** + * Callback executed on completion of NTLM login + * + * @return true to continue filter chaining, false otherwise + */ + protected abstract boolean onLoginComplete(HttpServletRequest req, HttpServletResponse res) + throws IOException; + + /** + * Validate the MD4 hash against local password + * + * @param type3Msg + * @param ntlmDetails + * @param authenticated + * @param md4hash + * + * @return true if password hash is valid, false otherwise + */ + protected boolean validateLocalHashedPassword(Type3NTLMMessage type3Msg, NTLMLogonDetails ntlmDetails, boolean authenticated, String md4hash) + { + // 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"); + } + 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); + + if (getLogger().isDebugEnabled()) + getLogger().debug((authenticated ? "Logged on" : "Logon failed") + " using NTLMSSP/NTLMv1"); + } + return authenticated; + } + + /** + * Perform an NTLMv1 hashed password check + * + * @param String md4hash + * @param byte[] challenge + * @param Type3NTLMMessage type3Msg + * @return boolean + */ + protected final boolean checkNTLMv1(String md4hash, byte[] challenge, Type3NTLMMessage type3Msg) + { + // 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 = 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) + { + // Hashed passwords match + return true; + } + } + + // Hashed passwords do not match + return false; + } + + /** + * Perform an NTLMv2 check + * + * @param String md4hash + * @param byte[] challenge + * @param Type3NTLMMessage type3Msg + * @return boolean + */ + protected final boolean checkNTLMv2(String md4hash, byte[] challenge, Type3NTLMMessage type3Msg) + { + 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) + { + // HMAC matches the client, user authenticated + return true; + } + } + } + catch (Exception ex) + { + if (getLogger().isDebugEnabled()) + getLogger().debug(ex); + } + + // NTLMv2 check failed + return false; + } + + /** + * Perform an NTLMv2 session key check + * + * @param String md4hash + * @param byte[] challenge + * @param Type3NTLMMessage type3Msg + * @return boolean + */ + protected final boolean checkNTLMv2SessionKey(String md4hash, byte[] challenge, Type3NTLMMessage type3Msg) + { + // 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) + { + // Hashed password check successful + return true; + } + } + + // Password check failed + return false; + } + + /** + * Get the stored MD4 hashed password for the user, or null if the user does not exist + * + * @param userName + * @param md4hash + * + * @return MD4 hash or null + */ + protected String getMD4Hash(String userName) + { + String md4hash = null; + + // Wrap the auth component calls in a transaction + UserTransaction tx = m_transactionService.getUserTransaction(); + try + { + tx.begin(); + + // Get the stored MD4 hashed password for the user, or null if the user does not exist + md4hash = m_authComponent.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; + } + + /** + * @param resp + * @param httpSess + * @throws IOException + */ + protected void restartLoginChallenge(HttpServletResponse res, HttpSession session) throws IOException + { + // Remove any existing session and NTLM details from the session + session.removeAttribute(NTLM_AUTH_SESSION); + session.removeAttribute(NTLM_AUTH_DETAILS); + + // Force the logon to start again + res.setHeader(WWW_AUTHENTICATE, AUTH_NTLM); + res.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + res.flushBuffer(); + } + + /** + * Map a client IP address to a domain + * + * @param clientIP String + * @return String + */ + protected final String mapClientAddressToDomain(String clientIP) + { + // Check if there are any domain mappings + if (m_secConfig != null && m_secConfig.hasDomainMappings() == false) + { + return null; + } + + // convert the client IP address to an integer value + int clientAddr = IPAddress.parseNumericAddress(clientIP); + for (DomainMapping domainMap : m_secConfig.getDomainMappings()) + { + if (domainMap.isMemberOfDomain(clientAddr)) + { + if (getLogger().isDebugEnabled()) + getLogger().debug("Mapped client IP " + clientIP + " to domain " + domainMap.getDomain()); + + return domainMap.getDomain(); + } + } + + if (getLogger().isDebugEnabled()) + getLogger().debug("Failed to map client IP " + clientIP + " to a domain"); + + // No domain mapping for the client address + return null; + } + + protected abstract Log getLogger(); +} diff --git a/source/java/org/alfresco/repo/webdav/auth/NTLMAuthenticationFilter.java b/source/java/org/alfresco/repo/webdav/auth/NTLMAuthenticationFilter.java index b39bbcb9f8..cc5dbf24f7 100644 --- a/source/java/org/alfresco/repo/webdav/auth/NTLMAuthenticationFilter.java +++ b/source/java/org/alfresco/repo/webdav/auth/NTLMAuthenticationFilter.java @@ -25,206 +25,45 @@ package org.alfresco.repo.webdav.auth; import java.io.IOException; -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.List; -import java.util.Random; -import javax.servlet.Filter; import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; -import javax.transaction.Status; import javax.transaction.UserTransaction; -import org.alfresco.filesys.ServerConfigurationBean; -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.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.model.ContentModel; -import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.repo.SessionUser; import org.alfresco.repo.security.authentication.AuthenticationException; -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.NTLMPassthruToken; -import org.alfresco.service.ServiceRegistry; -import org.alfresco.service.cmr.repository.InvalidNodeRefException; import org.alfresco.service.cmr.repository.NodeRef; -import org.alfresco.service.cmr.repository.NodeService; -import org.alfresco.service.cmr.security.AuthenticationService; -import org.alfresco.service.cmr.security.PersonService; -import org.alfresco.service.transaction.TransactionService; import org.apache.commons.codec.binary.Base64; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.web.context.WebApplicationContext; -import org.springframework.web.context.support.WebApplicationContextUtils; /** - * NTLM Authentication Filter Class + * WebDav NTLM Authentication Filter Class * * @author GKSpencer */ -public class NTLMAuthenticationFilter implements Filter +public class NTLMAuthenticationFilter extends BaseNTLMAuthenticationFilter { - // NTLM authentication session object names - - public static final String NTLM_AUTH_SESSION = "_alfNTLMAuthSess"; - public static final String NTLM_AUTH_DETAILS = "_alfNTLMDetails"; - // Authenticated user session object name - public final static String AUTHENTICATION_USER = "_alfDAVAuthTicket"; // Allow an authenitcation ticket to be passed as part of a request to bypass authentication - private static final String ARG_TICKET = "ticket"; - // NTLM flags mask, used to mask out features that are not supported - - private static final int NTLM_FLAGS = NTLM.Flag56Bit + NTLM.FlagLanManKey + NTLM.FlagNegotiateNTLM + - NTLM.FlagNegotiateOEM + NTLM.FlagNegotiateUnicode; - // Debug logging - private static Log logger = LogFactory.getLog(NTLMAuthenticationFilter.class); - // Servlet context, required to get authentication service - private ServletContext m_context; - - // File server configuration - - private ServerConfigurationBean m_srvConfig; - - // Various services required by NTLM authenticator - - private AuthenticationService m_authService; - private AuthenticationComponent m_authComponent; - private PersonService m_personService; - private NodeService m_nodeService; - private TransactionService m_transactionService; - - // Password encryptor - - private PasswordEncryptor m_encryptor = new PasswordEncryptor(); - - // Allow guest access - - private boolean m_allowGuest; - - // 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(); - - // Local server name, from either the file servers config or DNS host name - - private String m_srvName; - - /** - * Initialize the filter - * - * @param args FilterConfig - * @exception ServletException - */ - public void init(FilterConfig args) throws ServletException - { - // Save the servlet context, needed to get hold of the authentication service - - m_context = args.getServletContext(); - - // Setup the authentication context - - WebApplicationContext ctx = WebApplicationContextUtils.getRequiredWebApplicationContext(m_context); - - ServiceRegistry serviceRegistry = (ServiceRegistry) ctx.getBean(ServiceRegistry.SERVICE_REGISTRY); - m_nodeService = serviceRegistry.getNodeService(); - m_transactionService = serviceRegistry.getTransactionService(); - - m_authService = (AuthenticationService) ctx.getBean("authenticationService"); - m_authComponent = (AuthenticationComponent) ctx.getBean("authenticationComponent"); - m_personService = (PersonService) ctx.getBean("personService"); - - m_srvConfig = (ServerConfigurationBean) ctx.getBean(ServerConfigurationBean.SERVER_CONFIGURATION); - - // Check that the authentication component supports the required mode - - if ( m_authComponent.getNTLMMode() != NTLMMode.MD4_PROVIDER && - m_authComponent.getNTLMMode() != NTLMMode.PASS_THROUGH) - { - throw new ServletException("Required authentication mode not available"); - } - - // Get the local server name, try the file server config first - - if ( m_srvConfig != null) - { - m_srvName = m_srvConfig.getLocalServerName( true); - } - else - { - // Get the host name - - try - { - // Get the local host name - - m_srvName = InetAddress.getLocalHost().getHostName(); - - // Strip any domain name - - int pos = m_srvName.indexOf("."); - if ( pos != -1) - m_srvName = m_srvName.substring(0, pos - 1); - } - catch (UnknownHostException ex) - { - // Log the error - - if ( logger.isErrorEnabled()) - logger.error("NTLM filter, error getting local host name", ex); - } - - } - - // Check if the server name is valid - - if ( m_srvName == null || m_srvName.length() == 0) - throw new ServletException("Failed to get local server name"); - - // Check if guest access is to be allowed - - String guestAccess = args.getInitParameter("AllowGuest"); - if ( guestAccess != null) - { - m_allowGuest = Boolean.parseBoolean(guestAccess); - - // Debug - - if ( logger.isDebugEnabled() && m_allowGuest) - logger.debug("NTLM filter guest access allowed"); - } - } - /** * Run the filter * @@ -238,41 +77,30 @@ public class NTLMAuthenticationFilter implements Filter ServletException { // Get the HTTP request/response/session - HttpServletRequest req = (HttpServletRequest) sreq; HttpServletResponse resp = (HttpServletResponse) sresp; - HttpSession httpSess = req.getSession(true); - + // Check if there is an authorization header with an NTLM security blob - - String authHdr = req.getHeader("Authorization"); - boolean reqAuth = false; - - if ( authHdr != null && authHdr.startsWith("NTLM")) - reqAuth = true; + String authHdr = req.getHeader(AUTHORIZATION); + boolean reqAuth = (authHdr != null && authHdr.startsWith(AUTH_NTLM)); // Check if the user is already authenticated - WebDAVUser user = (WebDAVUser) httpSess.getAttribute(AUTHENTICATION_USER); - - if ( user != null && reqAuth == false) + if (user != null && reqAuth == false) { try { - // Debug - - if ( logger.isDebugEnabled()) + if (logger.isDebugEnabled()) logger.debug("User " + user.getUserName() + " validate ticket"); // Validate the user ticket - m_authService.validate( user.getTicket()); reqAuth = false; } catch (AuthenticationException ex) { - if ( logger.isErrorEnabled()) + if (logger.isErrorEnabled()) logger.error("Failed to validate user " + user.getUserName(), ex); reqAuth = true; @@ -281,87 +109,67 @@ public class NTLMAuthenticationFilter implements Filter // If the user has been validated and we do not require re-authentication then continue to // the next filter - - if ( reqAuth == false && user != null) + if (reqAuth == false && user != null) { - // Debug - - if ( logger.isDebugEnabled()) + if (logger.isDebugEnabled()) logger.debug("Authentication not required, chaining ..."); // Chain to the next filter - chain.doFilter(sreq, sresp); return; } // Check the authorization header - - if ( authHdr == null) { - + if (authHdr == null) + { // Check if the request includes an authentication ticket - - String ticket = req.getParameter( ARG_TICKET); - - if ( ticket != null && ticket.length() > 0) + String ticket = req.getParameter(ARG_TICKET); + if (ticket != null && ticket.length() != 0) { - // Debug - - if ( logger.isDebugEnabled()) + if (logger.isDebugEnabled()) logger.debug("Logon via ticket from " + req.getRemoteHost() + " (" + req.getRemoteAddr() + ":" + req.getRemotePort() + ")" + " ticket=" + ticket); UserTransaction tx = null; - try - { - // Validate the ticket - - m_authService.validate(ticket); - - // Need to create the User instance if not already available - - String currentUsername = m_authService.getCurrentUserName(); - - // Start a transaction - - tx = m_transactionService.getUserTransaction(); - tx.begin(); - - NodeRef personRef = m_personService.getPerson(currentUsername); - user = new WebDAVUser( currentUsername, m_authService.getCurrentTicket(), personRef); - NodeRef homeRef = (NodeRef) m_nodeService.getProperty(personRef, ContentModel.PROP_HOMEFOLDER); - - // Check that the home space node exists - else Login cannot proceed - - if (m_nodeService.exists(homeRef) == false) - { - throw new InvalidNodeRefException(homeRef); - } - user.setHomeNode(homeRef); - - tx.commit(); - tx = null; - - // Store the User object in the Session - the authentication servlet will then proceed - - req.getSession().setAttribute( AUTHENTICATION_USER, user); - - // Chain to the next filter - - chain.doFilter(sreq, sresp); - return; - } - catch (AuthenticationException authErr) + try + { + // Validate the ticket + m_authService.validate(ticket); + + if (user == null) + { + // Start a transaction + tx = m_transactionService.getUserTransaction(); + tx.begin(); + + // Need to create the User instance if not already available + String currentUsername = m_authService.getCurrentUserName(); + + NodeRef personRef = m_personService.getPerson(currentUsername); + user = new WebDAVUser(currentUsername, m_authService.getCurrentTicket(), personRef); + NodeRef homeRef = (NodeRef)m_nodeService.getProperty(personRef, ContentModel.PROP_HOMEFOLDER); + user.setHomeNode(homeRef); + + tx.commit(); + tx = null; + + // Store the User object in the Session - the authentication servlet will then proceed + req.getSession().setAttribute(AUTHENTICATION_USER, user); + } + + // Chain to the next filter + chain.doFilter(sreq, sresp); + return; + } + catch (AuthenticationException authErr) { - // Clear the user object to signal authentication failure - - user = null; + if (logger.isDebugEnabled()) + logger.debug("Failed to authenticate user ticket: " + authErr.getMessage(), authErr); } catch (Throwable e) { - // Clear the user object to signal authentication failure - - user = null; + if (logger.isDebugEnabled()) + logger.debug("Error during ticket validation and user creation: " + e.getMessage(), e); } finally { @@ -378,549 +186,153 @@ public class NTLMAuthenticationFilter implements Filter } } - // Debug - - if ( logger.isDebugEnabled()) + if (logger.isDebugEnabled()) logger.debug("New NTLM auth request from " + req.getRemoteHost() + " (" + req.getRemoteAddr() + ":" + req.getRemotePort() + ")"); // Send back a request for NTLM authentication - - resp.setHeader("WWW-Authenticate", "NTLM"); - resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - - resp.flushBuffer(); - return; + restartLoginChallenge(resp, httpSess); } else { // Decode the received NTLM blob and validate - - byte[] ntlmByts = Base64.decodeBase64( authHdr.substring(5).getBytes()); + final byte[] ntlmByts = Base64.decodeBase64(authHdr.substring(5).getBytes()); int ntlmTyp = NTLMMessage.isNTLMType(ntlmByts); - - if ( ntlmTyp == NTLM.Type1) + if (ntlmTyp == NTLM.Type1) { // Process the type 1 NTLM message - Type1NTLMMessage type1Msg = new Type1NTLMMessage(ntlmByts); processType1(type1Msg, req, resp, httpSess); - return; } - else if ( ntlmTyp == NTLM.Type3) + else if (ntlmTyp == NTLM.Type3) { // Process the type 3 NTLM message - Type3NTLMMessage type3Msg = new Type3NTLMMessage(ntlmByts); processType3(type3Msg, req, resp, httpSess, chain); - return; - } - } - - // Remove any existing session and NTLM details from the session - - httpSess.removeAttribute(NTLM_AUTH_SESSION); - httpSess.removeAttribute(NTLM_AUTH_DETAILS); - - // Force the logon to start again - - resp.setHeader("WWW-Authenticate", "NTLM"); - resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - - resp.flushBuffer(); - } - - /** - * Delete the servlet filter - */ - public void destroy() - { - } - - /** - * Process a type 1 NTLM message - * - * @param type1Msg Type1NTLMMessage - * @param req HttpServletRequest - * @param resp HttpServletResponse - * @param httpSess HttpSession - * @exception IOException - */ - private void processType1(Type1NTLMMessage type1Msg, HttpServletRequest req, HttpServletResponse resp, - HttpSession httpSess) throws IOException - { - // Debug - - if ( logger.isDebugEnabled()) - logger.debug("Received type1 " + type1Msg); - - // Get the existing NTLM details - - NTLMLogonDetails ntlmDetails = null; - - if ( httpSess != null) - { - ntlmDetails = (NTLMLogonDetails) httpSess.getAttribute(NTLM_AUTH_DETAILS); - } - - // Check if cached logon details are available - - if ( ntlmDetails != null && ntlmDetails.hasType2Message() && ntlmDetails.hasNTLMHashedPassword() && - ntlmDetails.hasAuthenticationToken()) - { - // Get the authentication server type2 response - - Type2NTLMMessage cachedType2 = ntlmDetails.getType2Message(); - - byte[] type2Bytes = cachedType2.getBytes(); - String ntlmBlob = "NTLM " + new String(Base64.encodeBase64(type2Bytes)); - - // Debug - - if ( logger.isDebugEnabled()) - logger.debug("Sending cached NTLM type2 to client - " + cachedType2); - - // Send back a request for NTLM authentication - - resp.setHeader("WWW-Authenticate", ntlmBlob); - resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - - resp.flushBuffer(); - return; - } - else - { - // Clear any cached logon details - - httpSess.removeAttribute(NTLM_AUTH_DETAILS); - - // Set the 8 byte challenge for the new logon request - - byte[] challenge = null; - NTLMPassthruToken authToken = null; - - if ( m_authComponent.getNTLMMode() == NTLMMode.MD4_PROVIDER) - { - // Generate a random 8 byte challenge - - challenge = new byte[8]; - DataPacker.putIntelLong(m_random.nextLong(), challenge, 0); } else { - // Create an authentication token for the new logon + if (logger.isDebugEnabled()) + logger.debug("NTLM blob not handled, restarting login challenge."); - authToken = new NTLMPassthruToken(); - - // Run the first stage of the passthru authentication to get the challenge - - m_authComponent.authenticate( authToken); - - // Get the challenge from the token - - if ( authToken.getChallenge() != null) - challenge = authToken.getChallenge().getBytes(); + restartLoginChallenge(resp, httpSess); } - - // Get the flags from the client request and mask out unsupported features - - int ntlmFlags = type1Msg.getFlags() & NTLM_FLAGS; - - // Build a type2 message to send back to the client, containing the challenge - - List tList = new ArrayList(); - tList.add(new TargetInfo(NTLM.TargetServer, m_srvName)); - - Type2NTLMMessage type2Msg = new Type2NTLMMessage(); - type2Msg.buildType2(ntlmFlags, m_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); - - httpSess.setAttribute(NTLM_AUTH_DETAILS, ntlmDetails); - - // Debug - - if ( logger.isDebugEnabled()) - logger.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)); - - resp.setHeader("WWW-Authenticate", ntlmBlob); - resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - - resp.flushBuffer(); - return; } } - /** - * Process a type 3 NTLM message - * - * @param type3Msg Type3NTLMMessage - * @param req HttpServletRequest - * @param resp HttpServletResponse - * @param httpSess HttpSession - * @param chain FilterChain - * @exception IOException - * @exception ServletException + /* (non-Javadoc) + * @see org.alfresco.repo.webdav.auth.BaseNTLMAuthenticationFilter#getSessionUser(javax.servlet.http.HttpSession) */ - private void processType3(Type3NTLMMessage type3Msg, HttpServletRequest req, HttpServletResponse resp, - HttpSession httpSess, FilterChain chain) throws IOException, ServletException + @Override + protected SessionUser getSessionUser(HttpSession session) { - // Debug + return (SessionUser)session.getAttribute(AUTHENTICATION_USER); + } + + /* (non-Javadoc) + * @see org.alfresco.repo.webdav.auth.BaseNTLMAuthenticationFilter#onValidate(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpSession) + */ + @Override + protected void onValidate(HttpServletRequest req, HttpSession session) + { + // nothing to do for webdav filter + } + + /* (non-Javadoc) + * @see org.alfresco.repo.webdav.auth.BaseNTLMAuthenticationFilter#onValidateFailed(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, javax.servlet.http.HttpSession) + */ + @Override + protected void onValidateFailed(HttpServletRequest req, HttpServletResponse res, HttpSession session) + throws IOException + { + // restart the login challenge process if validation fails + restartLoginChallenge(res, session); + } + + /* (non-Javadoc) + * @see org.alfresco.repo.webdav.auth.BaseNTLMAuthenticationFilter#createUserEnvironment(javax.servlet.http.HttpSession, java.lang.String) + */ + @Override + protected SessionUser createUserEnvironment(HttpSession session, String userName) + throws IOException, ServletException + { + Log logger = getLogger(); - if ( logger.isDebugEnabled()) - logger.debug("Received type3 " + type3Msg); + SessionUser user = null; - // Get the existing NTLM details + UserTransaction tx = m_transactionService.getUserTransaction(); - NTLMLogonDetails ntlmDetails = null; - WebDAVUser user = null; - - if ( httpSess != null) + try { - ntlmDetails = (NTLMLogonDetails) httpSess.getAttribute(NTLM_AUTH_DETAILS); - user = (WebDAVUser) httpSess.getAttribute(AUTHENTICATION_USER); + tx.begin(); + + // Get user details for the authenticated user + m_authComponent.setCurrentUser(userName.toLowerCase()); + + // The user name used may be a different case to the NTLM supplied user name, + // read the current user and use that name + userName = m_authComponent.getCurrentUserName(); + + // Setup User object and Home space ID etc. + NodeRef personNodeRef = m_personService.getPerson(userName); + String currentTicket = m_authService.getCurrentTicket(); + user = new WebDAVUser(userName, currentTicket, personNodeRef); + + NodeRef homeSpaceRef = (NodeRef) m_nodeService.getProperty(personNodeRef, ContentModel.PROP_HOMEFOLDER); + ((WebDAVUser)user).setHomeNode(homeSpaceRef); + + tx.commit(); } - - // Get the NTLM logon details - - String userName = type3Msg.getUserName(); - String workstation = type3Msg.getWorkstation(); - String domain = type3Msg.getDomain(); - - boolean authenticated = false; - - // Check if we are using cached details for the authentication - - if ( user != null && ntlmDetails != null && ntlmDetails.hasNTLMHashedPassword()) + catch (Throwable ex) { - // Check if the received NTLM hashed password matches the cached password - - byte[] ntlmPwd = type3Msg.getNTLMHash(); - byte[] cachedPwd = ntlmDetails.getNTLMHashedPassword(); - - if ( ntlmPwd != null) - { - if ( ntlmPwd.length == cachedPwd.length) - { - authenticated = true; - for ( int i = 0; i < ntlmPwd.length; i++) - { - if ( ntlmPwd[i] != cachedPwd[i]) - authenticated = false; - } - } - } - - // Debug - - if ( logger.isDebugEnabled()) - logger.debug("Using cached NTLM hash, authenticated = " + authenticated); - try { - // Debug - - if ( logger.isDebugEnabled()) - logger.debug("User " + user.getUserName() + " validate ticket"); - - // Validate the user ticket - - m_authService.validate( user.getTicket()); + tx.rollback(); } - catch (AuthenticationException ex) + catch (Exception ex2) { - if ( logger.isErrorEnabled()) - logger.error("Failed to validate user " + user.getUserName(), ex); - - // Restart the authentication - - httpSess.removeAttribute(NTLM_AUTH_SESSION); - httpSess.removeAttribute(NTLM_AUTH_DETAILS); - - // Force the logon to start again - - resp.setHeader("WWW-Authenticate", "NTLM"); - resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - - resp.flushBuffer(); - return; + logger.error("Failed to rollback transaction", ex2); } - - // Allow the user to access the requested page - - chain.doFilter( req, resp); - return; - } - else - { - // Cehck if we are using local MD4 password hashes or passthru authentication - - if ( m_authComponent.getNTLMMode() == NTLMMode.MD4_PROVIDER) + if (ex instanceof RuntimeException) { - // Wrap the auth componenet calls in a transaction - - UserTransaction tx = m_transactionService.getUserTransaction(); - String md4hash = null; - - try - { - tx.begin(); - - // Get the stored MD4 hashed password for the user, or null if the user does not exist - - md4hash = m_authComponent.getMD4HashedPassword(userName); - } - catch ( Throwable ex) { - - if ( logger.isDebugEnabled()) - logger.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 ( logger.isDebugEnabled()) - logger.debug(ex); - } - } - } - - // Check if we got a valid MD4 hashed password - - if ( md4hash != null) - { - // 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, ntlmDetails.getChallengeKey()); - } - catch (NoSuchAlgorithmException 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) - authenticated = true; - } - } - else - { - // Debug - - if ( logger.isDebugEnabled()) - logger.debug("User " + userName + " does not have Alfresco account"); - - // Bypass NTLM authentication and display the logon screen, user account does not - // exist in Alfresco - - authenticated = false; - } + throw (RuntimeException)ex; + } + else if (ex instanceof IOException) + { + throw (IOException)ex; + } + else if (ex instanceof ServletException) + { + throw (ServletException)ex; } else { - // 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 - - m_authComponent.authenticate(authToken); - authenticated = true; - } - catch (AuthenticationException ex) - { - // Debug - - 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) - { - UserTransaction tx = m_transactionService.getUserTransaction(); - - try - { - tx.begin(); - - // Get user details for the authenticated user - m_authComponent.setCurrentUser(userName.toLowerCase()); - - // The user name used may be a different case to the NTLM supplied user name, read the current - // user and use that name - userName = m_authComponent.getCurrentUserName(); - - // Setup User object and Home space ID etc. - NodeRef personNodeRef = m_personService.getPerson(userName); - String currentTicket = m_authService.getCurrentTicket(); - user = new WebDAVUser(userName, currentTicket, personNodeRef); - - NodeRef homeSpaceRef = (NodeRef) m_nodeService.getProperty(personNodeRef, ContentModel.PROP_HOMEFOLDER); - user.setHomeNode(homeSpaceRef); - - // commit - tx.commit(); - tx = null; - } - catch ( Throwable ex) { - - if ( logger.isDebugEnabled()) - logger.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 ( logger.isDebugEnabled()) - logger.debug(ex); - } - } - } - - // Store the user - - httpSess.setAttribute(AUTHENTICATION_USER, user); - - // Update the NTLM logon details in the session - - if ( ntlmDetails == null) - { - // No cached NTLM details - - ntlmDetails = new NTLMLogonDetails( userName, workstation, domain, false, m_srvName); - - httpSess.setAttribute(NTLM_AUTH_DETAILS, ntlmDetails); - - // Debug - - if ( logger.isDebugEnabled()) - logger.debug("No cached NTLM details, created"); - - } - else - { - // Update the cached NTLM details - - ntlmDetails.setDetails(userName, workstation, domain, false, m_srvName); - ntlmDetails.setNTLMHashedPassword(type3Msg.getNTLMHash()); - - // Debug - - if ( logger.isDebugEnabled()) - logger.debug("Updated cached NTLM details"); - } - - // Debug - - if ( logger.isDebugEnabled()) - logger.debug("User logged on via NTLM, " + ntlmDetails); - - // Allow the user to access the requested page - - chain.doFilter( req, resp); - return; - } - else - { - // Remove any existing session and NTLM details from the session - - httpSess.removeAttribute(NTLM_AUTH_SESSION); - httpSess.removeAttribute(NTLM_AUTH_DETAILS); - - // Force the logon to start again - - resp.setHeader("WWW-Authenticate", "NTLM"); - resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - - resp.flushBuffer(); - return; + throw new RuntimeException("Authentication setup failed", ex); } } - } + + // Store the user on the session + session.setAttribute(AUTHENTICATION_USER, user); + + return user; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.webdav.auth.BaseNTLMAuthenticationFilter#onLoginComplete(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) + */ + @Override + protected boolean onLoginComplete(HttpServletRequest req, HttpServletResponse res) throws IOException + { + // no futher processing to do, allow to complete + return true; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.webdav.auth.BaseNTLMAuthenticationFilter#getLogger() + */ + @Override + final protected Log getLogger() + { + return logger; + } } diff --git a/source/java/org/alfresco/repo/webdav/auth/WebDAVUser.java b/source/java/org/alfresco/repo/webdav/auth/WebDAVUser.java index 2d02972fa1..9dece83f05 100644 --- a/source/java/org/alfresco/repo/webdav/auth/WebDAVUser.java +++ b/source/java/org/alfresco/repo/webdav/auth/WebDAVUser.java @@ -27,6 +27,7 @@ package org.alfresco.repo.webdav.auth; import java.io.Serializable; +import org.alfresco.repo.SessionUser; import org.alfresco.service.cmr.repository.NodeRef; /** @@ -36,7 +37,7 @@ import org.alfresco.service.cmr.repository.NodeRef; * * @author GKSpencer */ -public class WebDAVUser implements Serializable +public class WebDAVUser implements SessionUser { private static final long serialVersionUID = -6948146071131901345L;