diff --git a/config/alfresco/subsystems/Authentication/alfrescoNtlm/alfresco-authentication-context.xml b/config/alfresco/subsystems/Authentication/alfrescoNtlm/alfresco-authentication-context.xml index 4f58461242..ca5b09a513 100644 --- a/config/alfresco/subsystems/Authentication/alfrescoNtlm/alfresco-authentication-context.xml +++ b/config/alfresco/subsystems/Authentication/alfrescoNtlm/alfresco-authentication-context.xml @@ -9,6 +9,9 @@ + + + ${alfresco.authentication.allowGuestLogin} diff --git a/source/java/org/alfresco/repo/security/authentication/AuthenticationComponentImpl.java b/source/java/org/alfresco/repo/security/authentication/AuthenticationComponentImpl.java index 983723d7bf..38bd5204cd 100644 --- a/source/java/org/alfresco/repo/security/authentication/AuthenticationComponentImpl.java +++ b/source/java/org/alfresco/repo/security/authentication/AuthenticationComponentImpl.java @@ -21,6 +21,7 @@ package org.alfresco.repo.security.authentication; import java.io.PrintWriter; import java.io.StringWriter; import java.util.Collections; +import java.util.List; import java.util.Set; import net.sf.acegisecurity.Authentication; @@ -31,18 +32,24 @@ import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.repo.security.authentication.ntlm.NLTMAuthenticator; +import org.alfresco.repo.tenant.TenantContextHolder; import org.alfresco.repo.tenant.TenantDisabledException; import org.alfresco.repo.tenant.TenantUtil; import org.alfresco.repo.tenant.TenantUtil.TenantRunAsWork; -import org.alfresco.repo.tenant.TenantContextHolder; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.util.Pair; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; public class AuthenticationComponentImpl extends AbstractAuthenticationComponent implements NLTMAuthenticator { + private static Log logger = LogFactory.getLog(AuthenticationComponentImpl.class); + private MutableAuthenticationDao authenticationDao; - + AuthenticationManager authenticationManager; + CompositePasswordEncoder passwordEncoder; public AuthenticationComponentImpl() { @@ -68,6 +75,11 @@ public class AuthenticationComponentImpl extends AbstractAuthenticationComponent { this.authenticationDao = authenticationDao; } + + public void setCompositePasswordEncoder(CompositePasswordEncoder passwordEncoder) + { + this.passwordEncoder = passwordEncoder; + } /** * Authenticate @@ -94,6 +106,36 @@ public class AuthenticationComponentImpl extends AbstractAuthenticationComponent UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( normalized == null ? userName : normalized, new String(password)); authenticationManager.authenticate(authentication); + + // check whether the user's password requires re-hashing + UserDetails userDetails = authenticationDao.loadUserByUsername(userName); + if (userDetails instanceof RepositoryAuthenticatedUser) + { + List hashIndicator = ((RepositoryAuthenticatedUser)userDetails).getHashIndicator(); + String preferredEncoding = passwordEncoder.getPreferredEncoding(); + + if (hashIndicator != null && !hashIndicator.isEmpty()) + { + // get the last encoding in the chain + String currentEncoding = hashIndicator.get(hashIndicator.size()-1); + + // if the encoding chain is longer than 1 (double hashed) or the + // current encoding is not the preferred encoding then re-generate + if (hashIndicator.size() > 1 || !currentEncoding.equals(preferredEncoding)) + { + // add transaction listener to re-hash the users password + HashPasswordTransactionListener txListener = new HashPasswordTransactionListener(userName, password); + txListener.setTransactionService(getTransactionService()); + txListener.setAuthenticationDao(authenticationDao); + AlfrescoTransactionSupport.bindListener(txListener); + if (logger.isDebugEnabled()) + { + logger.debug("New hashed password for user '" + userName + "' has been requested"); + } + } + } + } + return normalized; } }, tenantDomain); diff --git a/source/java/org/alfresco/repo/security/authentication/HashPasswordTransactionListener.java b/source/java/org/alfresco/repo/security/authentication/HashPasswordTransactionListener.java new file mode 100644 index 0000000000..a06c65542e --- /dev/null +++ b/source/java/org/alfresco/repo/security/authentication/HashPasswordTransactionListener.java @@ -0,0 +1,97 @@ +package org.alfresco.repo.security.authentication; + +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.repo.transaction.TransactionListener; +import org.alfresco.service.transaction.TransactionService; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +public class HashPasswordTransactionListener implements TransactionListener +{ + private static Log logger = LogFactory.getLog(HashPasswordTransactionListener.class); + + private final String username; + private final char[] password; + + private TransactionService transactionService; + private MutableAuthenticationDao authenticationDao; + + public HashPasswordTransactionListener(final String username, final char[] password) + { + this.username = username; + this.password = password; + } + + public void setTransactionService(TransactionService transactionService) + { + this.transactionService = transactionService; + } + + public void setAuthenticationDao(MutableAuthenticationDao authenticationDao) + { + this.authenticationDao = authenticationDao; + } + + @Override + public void flush() + { + // nothing to do + } + + @Override + public void beforeCommit(boolean readOnly) + { + // nothing to do + } + + @Override + public void beforeCompletion() + { + // nothing to do + } + + @Override + public void afterCommit() + { + // get transaction helper and force it to be writable in case system is in read only mode + RetryingTransactionHelper txHelper = transactionService.getRetryingTransactionHelper(); + txHelper.setForceWritable(true); + txHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + AuthenticationUtil.pushAuthentication(); + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + try + { + if (logger.isDebugEnabled()) + { + logger.debug("Re-hashing password for user: " + username); + } + + // update the users password to force a new hash to be generated + authenticationDao.updateUser(username, password); + + if (logger.isDebugEnabled()) + { + logger.debug("Password for user '" + username + "' has been re-hashed following login"); + } + + return null; + } + finally + { + AuthenticationUtil.popAuthentication(); + } + } + }, false, true); + } + + @Override + public void afterRollback() + { + // nothing to do + } +} diff --git a/source/test-java/org/alfresco/repo/security/authentication/AuthenticationTest.java b/source/test-java/org/alfresco/repo/security/authentication/AuthenticationTest.java index 5b4b0559e0..e33cebc00e 100644 --- a/source/test-java/org/alfresco/repo/security/authentication/AuthenticationTest.java +++ b/source/test-java/org/alfresco/repo/security/authentication/AuthenticationTest.java @@ -24,6 +24,7 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Date; import java.util.HashMap; +import java.util.List; import java.util.Map; import javax.transaction.Status; @@ -39,7 +40,6 @@ import net.sf.acegisecurity.DisabledException; import net.sf.acegisecurity.LockedException; import net.sf.acegisecurity.UserDetails; import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken; -import net.sf.acegisecurity.providers.encoding.PasswordEncoder; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.model.ContentModel; @@ -1835,6 +1835,112 @@ public class AuthenticationTest extends TestCase } } + + /** + * Tests the scenario where a user logs in after the system has been upgraded. + * Their password should get re-hashed using the preferred encoding. + */ + public void testRehashedPasswordOnAuthentication() throws Exception + { + // create the Andy authentication + assertNull(authenticationComponent.getCurrentAuthentication()); + authenticationComponent.setSystemUserAsCurrentUser(); + pubAuthenticationService.createAuthentication("Andy", "auth1".toCharArray()); + + // find the node representing the Andy user and it's properties + NodeRef andyUserNodeRef = getAndyUserNodeRef(); + assertNotNull(andyUserNodeRef); + + // ensure the properties are in the state we're expecting + Map userProps = nodeService.getProperties(andyUserNodeRef); + String passwordProp = (String)userProps.get(ContentModel.PROP_PASSWORD); + assertNull("Expected the password property to be null", passwordProp); + String password2Prop = (String)userProps.get(ContentModel.PROP_PASSWORD_SHA256); + assertNull("Expected the password2 property to be null", password2Prop); + String passwordHashProp = (String)userProps.get(ContentModel.PROP_PASSWORD_HASH); + assertNotNull("Expected the passwordHash property to be populated", passwordHashProp); + List hashIndicatorProp = (List)userProps.get(ContentModel.PROP_HASH_INDICATOR); + assertNotNull("Expected the hashIndicator property to be populated", hashIndicatorProp); + + // re-generate an md4 hashed password + MD4PasswordEncoderImpl md4PasswordEncoder = new MD4PasswordEncoderImpl(); + String md4Password = md4PasswordEncoder.encodePassword("auth1", null); + + // re-generate a sha256 hashed password + String salt = (String)userProps.get(ContentModel.PROP_SALT); + ShaPasswordEncoderImpl sha256PasswordEncoder = new ShaPasswordEncoderImpl(256); + String sha256Password = sha256PasswordEncoder.encodePassword("auth1", salt); + + // change the underlying user object to represent state in previous release + userProps.put(ContentModel.PROP_PASSWORD, md4Password); + userProps.put(ContentModel.PROP_PASSWORD_SHA256, sha256Password); + userProps.remove(ContentModel.PROP_PASSWORD_HASH); + userProps.remove(ContentModel.PROP_HASH_INDICATOR); + nodeService.setProperties(andyUserNodeRef, userProps); + + // make sure the changes took effect + Map updatedProps = nodeService.getProperties(andyUserNodeRef); + String usernameProp = (String)updatedProps.get(ContentModel.PROP_USER_USERNAME); + assertEquals("Expected the username property to be 'Andy'", "Andy", usernameProp); + passwordProp = (String)updatedProps.get(ContentModel.PROP_PASSWORD); + assertNotNull("Expected the password property to be populated", passwordProp); + password2Prop = (String)updatedProps.get(ContentModel.PROP_PASSWORD_SHA256); + assertNotNull("Expected the password2 property to be populated", password2Prop); + passwordHashProp = (String)updatedProps.get(ContentModel.PROP_PASSWORD_HASH); + assertNull("Expected the passwordHash property to be null", passwordHashProp); + hashIndicatorProp = (List)updatedProps.get(ContentModel.PROP_HASH_INDICATOR); + assertNull("Expected the hashIndicator property to be null", hashIndicatorProp); + + // authenticate the user + authenticationComponent.clearCurrentSecurityContext(); + pubAuthenticationService.authenticate("Andy", "auth1".toCharArray()); + assertEquals("Andy", authenticationService.getCurrentUserName()); + + // commit the transaction to invoke the password hashing of the user + userTransaction.commit(); + + // start another transaction and change to system user + userTransaction = transactionService.getUserTransaction(); + userTransaction.begin(); + authenticationComponent.setSystemUserAsCurrentUser(); + + // verify that the new properties are populated and the old ones are cleaned up + Map upgradedProps = nodeService.getProperties(andyUserNodeRef); + passwordProp = (String)upgradedProps.get(ContentModel.PROP_PASSWORD); + assertNull("Expected the password property to be null", passwordProp); + password2Prop = (String)upgradedProps.get(ContentModel.PROP_PASSWORD_SHA256); + assertNull("Expected the password2 property to be null", password2Prop); + passwordHashProp = (String)upgradedProps.get(ContentModel.PROP_PASSWORD_HASH); + assertNotNull("Expected the passwordHash property to be populated", passwordHashProp); + hashIndicatorProp = (List)upgradedProps.get(ContentModel.PROP_HASH_INDICATOR); + assertNotNull("Expected the hashIndicator property to be populated", hashIndicatorProp); + assertTrue("Expected there to be a single hash indicator entry", (hashIndicatorProp.size() == 1)); + String preferredEncoding = compositePasswordEncoder.getPreferredEncoding(); + String hashEncoding = (String)hashIndicatorProp.get(0); + assertEquals("Expected hash indicator to be '" + preferredEncoding + "' but it was: " + hashEncoding, + preferredEncoding, hashEncoding); + + // delete the user and clear the security context + this.deleteAndy(); + authenticationComponent.clearCurrentSecurityContext(); + } + + private NodeRef getAndyUserNodeRef() + { + RepositoryAuthenticationDao dao = new RepositoryAuthenticationDao(); + dao.setTransactionService(transactionService); + dao.setAuthorityService(authorityService); + dao.setTenantService(tenantService); + dao.setNodeService(nodeService); + dao.setNamespaceService(getNamespacePrefixReolsver("")); + dao.setCompositePasswordEncoder(compositePasswordEncoder); + dao.setPolicyComponent(policyComponent); + dao.setAuthenticationCache(authenticationCache); + dao.setSingletonCache(immutableSingletonCache); + + return dao.getUserOrNull("Andy"); + } + private String getUserName(Authentication authentication) { String username = authentication.getPrincipal().toString();