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();