Merged 5.1-MC1 (5.1.0) to HEAD (5.1)

119045 adavis: Merged 5.1.N (5.1.1) to 5.1-MC1 (5.1.0)
      117327 adavis: Merged 5.0.2-CLOUD42 (Cloud ) to 5.1.N (5.1.1)
         117235 adavis: Merged 5.0.2-CLOUD (Cloud ) to 5.0.2-CLOUD42 (Cloud )
            114504 adavis: Merged BCRYPT to 5.0.2-CLOUD (PARTIAL MERGE)
               113666 gjames: Initial integration of CompositePasswordEncoder for MNT-14892


git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@119883 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
This commit is contained in:
Jean-Pierre Huynh
2015-12-10 09:57:42 +00:00
parent 01ffcc2511
commit d97bed0aaa
9 changed files with 468 additions and 84 deletions

View File

@@ -76,13 +76,34 @@
</property> </property>
</bean> </bean>
<bean id="compositePasswordEncoder" class="org.alfresco.repo.security.authentication.CompositePasswordEncoder" init-method="init">
<property name="encoders">
<map>
<entry key="md4">
<bean class="org.alfresco.repo.security.authentication.MD4PasswordEncoderImpl" />
</entry>
<entry key="sha256" value-ref="sha256PasswordEncoder" />
<entry key="bcrypt15">
<bean class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder">
<constructor-arg value="15" />
</bean>
</entry>
<!--entry key="bcrypt20">
<bean class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder">
<constructor-arg value="20" />
</bean>
</entry-->
</map>
</property>
<property name="preferredEncoding" value="bcrypt15" />
</bean>
<bean id="authenticationDao" class="org.alfresco.repo.security.authentication.RepositoryAuthenticationDao"> <bean id="authenticationDao" class="org.alfresco.repo.security.authentication.RepositoryAuthenticationDao">
<property name="nodeService" ref="nodeService" /> <property name="nodeService" ref="nodeService" />
<property name="authorityService" ref="authorityService" /> <property name="authorityService" ref="authorityService" />
<property name="tenantService" ref="tenantService" /> <property name="tenantService" ref="tenantService" />
<property name="namespaceService" ref="namespaceService" /> <property name="namespaceService" ref="namespaceService" />
<property name="passwordEncoder" ref="passwordEncoder" /> <property name="compositePasswordEncoder" ref="compositePasswordEncoder" />
<property name="sha256PasswordEncoder" ref="sha256PasswordEncoder" />
<property name="policyComponent" ref="policyComponent" /> <property name="policyComponent" ref="policyComponent" />
<property name="authenticationCache" ref="authenticationCache" /> <property name="authenticationCache" ref="authenticationCache" />
<property name="singletonCache" ref="immutableSingletonCache"/> <property name="singletonCache" ref="immutableSingletonCache"/>
@@ -147,17 +168,14 @@
</bean> </bean>
<!-- We provide a DAO to plug into the Acegi DaoAuthenticationProvider --> <!-- We provide a DAO to plug into the Acegi DaoAuthenticationProvider -->
<bean id="daoAuthenticationProvider" class="org.alfresco.repo.security.authentication.RepositoryAuthenticationProvider">
<bean id="daoAuthenticationProvider" class="net.sf.acegisecurity.providers.dao.DaoAuthenticationProvider">
<property name="authenticationDao"> <property name="authenticationDao">
<ref bean="authenticationDao" /> <ref bean="authenticationDao" />
</property> </property>
<property name="saltSource"> <property name="saltSource">
<ref bean="saltSource" /> <ref bean="saltSource" />
</property> </property>
<property name="passwordEncoder"> <property name="compositePasswordEncoder" ref="compositePasswordEncoder" />
<ref bean="passwordEncoder" />
</property>
</bean> </bean>
<!-- The DAO also acts as a salt provider. --> <!-- The DAO also acts as a salt provider. -->

View File

@@ -19,11 +19,14 @@
package org.alfresco.repo.security.authentication; package org.alfresco.repo.security.authentication;
import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ContentModel;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.ParameterCheck; import org.alfresco.util.ParameterCheck;
import org.alfresco.util.PropertyCheck; import org.alfresco.util.PropertyCheck;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -31,41 +34,22 @@ import java.util.Map;
* A configurable password encoding that delegates the encoding to a Map of * A configurable password encoding that delegates the encoding to a Map of
* configured encoders. * configured encoders.
* *
* @Author Gethin James * @author Gethin James
*/ */
public class CompositePasswordEncoder public class CompositePasswordEncoder
{ {
private static Log logger = LogFactory.getLog(CompositePasswordEncoder.class); private static Log logger = LogFactory.getLog(CompositePasswordEncoder.class);
private Map<String,Object> encoders; private Map<String,Object> encoders;
private String preferredEncoding; private String preferredEncoding;
private String legacyEncoding;
private String legacyEncodingProperty;
public void setLegacyEncoding(String legacyEncoding) public static final List<String> SHA256 = Arrays.asList("sha256");
{ public static final List<String> MD4 = Arrays.asList("md4");
this.legacyEncoding = legacyEncoding;
}
public void setLegacyEncodingProperty(String legacyEncodingProperty)
{
this.legacyEncodingProperty = legacyEncodingProperty;
}
public String getPreferredEncoding() public String getPreferredEncoding()
{ {
return preferredEncoding; return preferredEncoding;
} }
public String getLegacyEncoding()
{
return legacyEncoding;
}
public String getLegacyEncodingProperty()
{
return legacyEncodingProperty;
}
public void setPreferredEncoding(String preferredEncoding) public void setPreferredEncoding(String preferredEncoding)
{ {
this.preferredEncoding = preferredEncoding; this.preferredEncoding = preferredEncoding;
@@ -77,13 +61,17 @@ public class CompositePasswordEncoder
} }
/** /**
* Is this the preferred encoding ? * Is the preferred encoding the last encoding to be used.
* @param encoding a String representing the encoding * @param hashIndicator a List<String></String> representing the encoding
* @return true if is correct * @return true if is correct
*/ */
public boolean isPreferredEncoding(String encoding) public boolean lastEncodingIsPreferred(List<String> hashIndicator)
{ {
return preferredEncoding.equals(encoding); if (hashIndicator!= null && hashIndicator.size() > 0 && preferredEncoding.equals(hashIndicator.get(hashIndicator.size()-1)))
{
return true;
}
return false;
} }
/** /**
@@ -93,8 +81,6 @@ public class CompositePasswordEncoder
{ {
PropertyCheck.mandatory(this, "encoders", encoders); PropertyCheck.mandatory(this, "encoders", encoders);
PropertyCheck.mandatory(this, "preferredEncoding", preferredEncoding); PropertyCheck.mandatory(this, "preferredEncoding", preferredEncoding);
PropertyCheck.mandatory(this, "legacyEncoding", legacyEncoding);
PropertyCheck.mandatory(this, "legacyEncodingProperty", legacyEncodingProperty);
} }
/** /**
@@ -118,6 +104,17 @@ public class CompositePasswordEncoder
return encoded; return encoded;
} }
/**
* Encodes a password in the preferred encoding.
* @param rawPassword mandatory password
* @param salt optional salt
* @return Encoded password
*/
public String encodePreferred(String rawPassword, Object salt)
{
return encode(getPreferredEncoding(), rawPassword, salt);
}
/** /**
* Encode a password using the specified encoderKey * Encode a password using the specified encoderKey
* @param encoderKey the encoder to use * @param encoderKey the encoder to use

View File

@@ -0,0 +1,54 @@
/*
* Copyright (C) 2005-2016 Alfresco Software Limited.
*
* This file is part of Alfresco
*
* 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/>.
*/
package org.alfresco.repo.security.authentication;
import net.sf.acegisecurity.GrantedAuthority;
import net.sf.acegisecurity.providers.dao.User;
import java.io.Serializable;
import java.util.List;
/**
* A user authenticated by the Alfresco repository using RepositoryAuthenticationDao
* @author Gethin James
*/
public class RepositoryAuthenticatedUser extends User
{
private List<String> hashIndicator;
private Serializable salt;
public Serializable getSalt()
{
return salt;
}
public List<String> getHashIndicator()
{
return hashIndicator;
}
public RepositoryAuthenticatedUser(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, GrantedAuthority[] authorities, List<String> hashIndicator, Serializable salt) throws IllegalArgumentException
{
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
this.hashIndicator = hashIndicator;
this.salt = salt;
}
}

View File

@@ -19,6 +19,8 @@
package org.alfresco.repo.security.authentication; package org.alfresco.repo.security.authentication;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@@ -29,7 +31,6 @@ import net.sf.acegisecurity.GrantedAuthorityImpl;
import net.sf.acegisecurity.UserDetails; import net.sf.acegisecurity.UserDetails;
import net.sf.acegisecurity.providers.dao.User; import net.sf.acegisecurity.providers.dao.User;
import net.sf.acegisecurity.providers.dao.UsernameNotFoundException; import net.sf.acegisecurity.providers.dao.UsernameNotFoundException;
import net.sf.acegisecurity.providers.encoding.PasswordEncoder;
import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ContentModel; import org.alfresco.model.ContentModel;
@@ -55,6 +56,7 @@ import org.alfresco.service.namespace.RegexQNamePattern;
import org.alfresco.service.transaction.TransactionService; import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.EqualsHelper; import org.alfresco.util.EqualsHelper;
import org.alfresco.util.GUID; import org.alfresco.util.GUID;
import org.alfresco.util.Pair;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.InitializingBean;
@@ -75,13 +77,12 @@ public class RepositoryAuthenticationDao implements MutableAuthenticationDao, In
protected NodeService nodeService; protected NodeService nodeService;
protected TenantService tenantService; protected TenantService tenantService;
protected NamespacePrefixResolver namespacePrefixResolver; protected NamespacePrefixResolver namespacePrefixResolver;
protected PasswordEncoder passwordEncoder;
protected PasswordEncoder sha256PasswordEncoder;
protected PolicyComponent policyComponent; protected PolicyComponent policyComponent;
private TransactionService transactionService; private TransactionService transactionService;
private CompositePasswordEncoder compositePasswordEncoder;
// note: cache is tenant-aware (if using TransctionalCache impl) // note: cache is tenant-aware (if using TransctionalCache impl)
private SimpleCache<String, NodeRef> singletonCache; // eg. for user folder nodeRef private SimpleCache<String, NodeRef> singletonCache; // eg. for user folder nodeRef
private final String KEY_USERFOLDER_NODEREF = "key.userfolder.noderef"; private final String KEY_USERFOLDER_NODEREF = "key.userfolder.noderef";
@@ -117,16 +118,6 @@ public class RepositoryAuthenticationDao implements MutableAuthenticationDao, In
{ {
this.singletonCache = singletonCache; this.singletonCache = singletonCache;
} }
public void setPasswordEncoder(PasswordEncoder passwordEncoder)
{
this.passwordEncoder = passwordEncoder;
}
public void setSha256PasswordEncoder(PasswordEncoder passwordEncoder)
{
this.sha256PasswordEncoder = passwordEncoder;
}
public void setPolicyComponent(PolicyComponent policyComponent) public void setPolicyComponent(PolicyComponent policyComponent)
{ {
@@ -143,6 +134,11 @@ public class RepositoryAuthenticationDao implements MutableAuthenticationDao, In
this.transactionService = transactionService; this.transactionService = transactionService;
} }
public void setCompositePasswordEncoder(CompositePasswordEncoder compositePasswordEncoder)
{
this.compositePasswordEncoder = compositePasswordEncoder;
}
public void afterPropertiesSet() throws Exception public void afterPropertiesSet() throws Exception
{ {
this.policyComponent.bindClassBehaviour( this.policyComponent.bindClassBehaviour(
@@ -172,6 +168,15 @@ public class RepositoryAuthenticationDao implements MutableAuthenticationDao, In
{ {
return userDetails; return userDetails;
} }
if (userDetails instanceof RepositoryAuthenticatedUser)
{
RepositoryAuthenticatedUser repoUser = (RepositoryAuthenticatedUser) userDetails;
return new RepositoryAuthenticatedUser(userDetails.getUsername(), userDetails.getPassword(), userDetails.isEnabled(),
userDetails.isAccountNonExpired(), false,
userDetails.isAccountNonLocked(), userDetails.getAuthorities(), repoUser.getHashIndicator(), repoUser.getSalt());
}
// If the credentials have expired, we must return a copy with the flag set // If the credentials have expired, we must return a copy with the flag set
return new User(userDetails.getUsername(), userDetails.getPassword(), userDetails.isEnabled(), return new User(userDetails.getUsername(), userDetails.getPassword(), userDetails.isEnabled(),
userDetails.isAccountNonExpired(), false, userDetails.isAccountNonExpired(), false,
@@ -246,12 +251,12 @@ public class RepositoryAuthenticationDao implements MutableAuthenticationDao, In
// Extract values from the query results // Extract values from the query results
NodeRef userRef = tenantService.getName(results.get(0).getChildRef()); NodeRef userRef = tenantService.getName(results.get(0).getChildRef());
Map<QName, Serializable> properties = nodeService.getProperties(userRef); Map<QName, Serializable> properties = nodeService.getProperties(userRef);
String password = DefaultTypeConverter.INSTANCE.convert(String.class, Pair<List<String>, String> hashPassword = determinePasswordHash(properties);
properties.get(ContentModel.PROP_PASSWORD));
// Report back the user name as stored on the user // Report back the user name as stored on the user
String userName = DefaultTypeConverter.INSTANCE.convert(String.class, String userName = DefaultTypeConverter.INSTANCE.convert(String.class,
properties.get(ContentModel.PROP_USER_USERNAME)); properties.get(ContentModel.PROP_USER_USERNAME));
Serializable salt = properties.get(ContentModel.PROP_SALT);
GrantedAuthority[] gas = new GrantedAuthority[1]; GrantedAuthority[] gas = new GrantedAuthority[1];
gas[0] = new GrantedAuthorityImpl("ROLE_AUTHENTICATED"); gas[0] = new GrantedAuthorityImpl("ROLE_AUTHENTICATED");
@@ -261,14 +266,16 @@ public class RepositoryAuthenticationDao implements MutableAuthenticationDao, In
Date credentialsExpiryDate = getCredentialsExpiryDate(userName, properties, isAdminAuthority); Date credentialsExpiryDate = getCredentialsExpiryDate(userName, properties, isAdminAuthority);
boolean credentialsHaveNotExpired = (credentialsExpiryDate == null || credentialsExpiryDate.getTime() >= System.currentTimeMillis()); boolean credentialsHaveNotExpired = (credentialsExpiryDate == null || credentialsExpiryDate.getTime() >= System.currentTimeMillis());
UserDetails ud = new User( UserDetails ud = new RepositoryAuthenticatedUser(
userName, userName,
password, hashPassword.getSecond(),
getEnabled(userName, properties, isAdminAuthority), getEnabled(userName, properties, isAdminAuthority),
!getHasExpired(userName, properties, isAdminAuthority), !getHasExpired(userName, properties, isAdminAuthority),
credentialsHaveNotExpired, credentialsHaveNotExpired,
!getLocked(userName, properties, isAdminAuthority), !getLocked(userName, properties, isAdminAuthority),
gas); gas,
hashPassword.getFirst(),
salt);
cacheEntry = new CacheEntry(userRef, ud, credentialsExpiryDate); cacheEntry = new CacheEntry(userRef, ud, credentialsExpiryDate);
// Only cache positive results // Only cache positive results
@@ -282,6 +289,78 @@ public class RepositoryAuthenticationDao implements MutableAuthenticationDao, In
return transactionService.getRetryingTransactionHelper().doInTransaction(new SearchUserNameCallback(), true); return transactionService.getRetryingTransactionHelper().doInTransaction(new SearchUserNameCallback(), true);
} }
/**
* Should we rehash the password by updating the properties
* @param properties
* @return
*/
public boolean rehashedPassword(Map<QName, Serializable> properties)
{
List<String> hashIndicator = (List<String>) properties.get(ContentModel.PROP_HASH_INDICATOR);
Pair<List<String>, String> passwordHash = determinePasswordHash(properties);
if (!compositePasswordEncoder.lastEncodingIsPreferred(passwordHash.getFirst()))
{
//We need to double hash
List<String> nowHashed = new ArrayList<String>();
nowHashed.addAll(passwordHash.getFirst());
nowHashed.add(compositePasswordEncoder.getPreferredEncoding());
Object salt = properties.get(ContentModel.PROP_SALT);
properties.put(ContentModel.PROP_PASSWORD_HASH, compositePasswordEncoder.encodePreferred(new String(passwordHash.getSecond()), salt));
properties.put(ContentModel.PROP_HASH_INDICATOR, (Serializable)nowHashed);
properties.remove(ContentModel.PROP_PASSWORD);
properties.remove(ContentModel.PROP_PASSWORD_SHA256);
return true;
}
if (hashIndicator == null)
{
//Already the preferred encoding, just set it
properties.put(ContentModel.PROP_HASH_INDICATOR, (Serializable)passwordHash.getFirst());
properties.put(ContentModel.PROP_PASSWORD_HASH, passwordHash.getSecond());
properties.remove(ContentModel.PROP_PASSWORD);
properties.remove(ContentModel.PROP_PASSWORD_SHA256);
return true;
}
return false;
}
/**
* Where is the password and how is it encoded?
* @param properties
* @return
*/
protected Pair<List<String>, String> determinePasswordHash(Map<QName, Serializable> properties)
{
List<String> hashIndicator = (List<String>) properties.get(ContentModel.PROP_HASH_INDICATOR);
if (hashIndicator != null && hashIndicator.size()>0)
{
//We have hashed the value so get it.
return new Pair<>(hashIndicator,DefaultTypeConverter.INSTANCE.convert(String.class, properties.get(ContentModel.PROP_PASSWORD_HASH)));
}
else
{
String passHash = DefaultTypeConverter.INSTANCE.convert(String.class, properties.get(ContentModel.PROP_PASSWORD_SHA256));
if (passHash != null)
{
//We have a SHA256 so use it
return new Pair<>(CompositePasswordEncoder.SHA256,passHash);
}
else
{
passHash = DefaultTypeConverter.INSTANCE.convert(String.class, properties.get(ContentModel.PROP_PASSWORD));
if (passHash != null)
{
//Use MD4
return new Pair<>(CompositePasswordEncoder.MD4,passHash);
}
}
}
throw new AlfrescoRuntimeException("Unable to find a user password, please check your repository authentication settings."
+ "(PreferredEncoding="+compositePasswordEncoder.getPreferredEncoding()+")");
}
@Override @Override
public void createUser(String caseSensitiveUserName, char[] rawPassword) throws AuthenticationException public void createUser(String caseSensitiveUserName, char[] rawPassword) throws AuthenticationException
{ {
@@ -297,8 +376,8 @@ public class RepositoryAuthenticationDao implements MutableAuthenticationDao, In
properties.put(ContentModel.PROP_USER_USERNAME, caseSensitiveUserName); properties.put(ContentModel.PROP_USER_USERNAME, caseSensitiveUserName);
String salt = GUID.generate(); String salt = GUID.generate();
properties.put(ContentModel.PROP_SALT, salt); properties.put(ContentModel.PROP_SALT, salt);
properties.put(ContentModel.PROP_PASSWORD, passwordEncoder.encodePassword(new String(rawPassword), null)); properties.put(ContentModel.PROP_PASSWORD_HASH, compositePasswordEncoder.encodePreferred(new String(rawPassword), salt));
properties.put(ContentModel.PROP_PASSWORD_SHA256, sha256PasswordEncoder.encodePassword(new String(rawPassword), salt)); properties.put(ContentModel.PROP_HASH_INDICATOR, (Serializable) Arrays.asList(compositePasswordEncoder.getPreferredEncoding()));
properties.put(ContentModel.PROP_ACCOUNT_EXPIRES, Boolean.valueOf(false)); properties.put(ContentModel.PROP_ACCOUNT_EXPIRES, Boolean.valueOf(false));
properties.put(ContentModel.PROP_CREDENTIALS_EXPIRE, Boolean.valueOf(false)); properties.put(ContentModel.PROP_CREDENTIALS_EXPIRE, Boolean.valueOf(false));
properties.put(ContentModel.PROP_ENABLED, Boolean.valueOf(true)); properties.put(ContentModel.PROP_ENABLED, Boolean.valueOf(true));
@@ -363,10 +442,10 @@ public class RepositoryAuthenticationDao implements MutableAuthenticationDao, In
String salt = GUID.generate(); String salt = GUID.generate();
properties.remove(ContentModel.PROP_SALT); properties.remove(ContentModel.PROP_SALT);
properties.put(ContentModel.PROP_SALT, salt); properties.put(ContentModel.PROP_SALT, salt);
properties.put(ContentModel.PROP_PASSWORD_HASH, compositePasswordEncoder.encodePreferred(new String(rawPassword), salt));
properties.put(ContentModel.PROP_HASH_INDICATOR, compositePasswordEncoder.getPreferredEncoding());
properties.remove(ContentModel.PROP_PASSWORD); properties.remove(ContentModel.PROP_PASSWORD);
properties.put(ContentModel.PROP_PASSWORD, passwordEncoder.encodePassword(new String(rawPassword), null));
properties.remove(ContentModel.PROP_PASSWORD_SHA256); properties.remove(ContentModel.PROP_PASSWORD_SHA256);
properties.put(ContentModel.PROP_PASSWORD_SHA256, sha256PasswordEncoder.encodePassword(new String(rawPassword), salt));
nodeService.setProperties(userRef, properties); nodeService.setProperties(userRef, properties);
} }

View File

@@ -0,0 +1,56 @@
/*
* Copyright (C) 2005-2016 Alfresco Software Limited.
*
* This file is part of Alfresco
*
* 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/>.
*/
package org.alfresco.repo.security.authentication;
import net.sf.acegisecurity.Authentication;
import net.sf.acegisecurity.UserDetails;
import net.sf.acegisecurity.providers.dao.DaoAuthenticationProvider;
import net.sf.acegisecurity.providers.dao.SaltSource;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* A DaoAuthenticationProvider that makes use of a CompositePasswordEncoder to check the
* password is correct.
*
* @author Gethin James
*/
public class RepositoryAuthenticationProvider extends DaoAuthenticationProvider
{
private static Log logger = LogFactory.getLog(RepositoryAuthenticationProvider.class);
CompositePasswordEncoder compositePasswordEncoder;
public void setCompositePasswordEncoder(CompositePasswordEncoder compositePasswordEncoder)
{
this.compositePasswordEncoder = compositePasswordEncoder;
}
@Override
protected boolean isPasswordCorrect(Authentication authentication, UserDetails user)
{
if (user instanceof RepositoryAuthenticatedUser)
{
RepositoryAuthenticatedUser repoUser = (RepositoryAuthenticatedUser) user;
return compositePasswordEncoder.matchesPassword(authentication.getCredentials().toString(),user.getPassword(), repoUser.getSalt(), repoUser.getHashIndicator() );
}
logger.error("Password check error for "+user.getUsername()+" unknown user type: "+user.getClass().getName());
return false;
}
}

View File

@@ -101,6 +101,8 @@ public class AllUnitTestsSuite extends TestSuite
suite.addTest(new JUnit4TestAdapter(org.alfresco.util.BeanExtenderUnitTest.class)); suite.addTest(new JUnit4TestAdapter(org.alfresco.util.BeanExtenderUnitTest.class));
suite.addTest(new JUnit4TestAdapter(org.alfresco.repo.search.impl.solr.SpellCheckDecisionManagerTest.class)); suite.addTest(new JUnit4TestAdapter(org.alfresco.repo.search.impl.solr.SpellCheckDecisionManagerTest.class));
suite.addTest(new JUnit4TestAdapter(org.alfresco.repo.search.impl.solr.SolrStoreMappingWrapperTest.class)); suite.addTest(new JUnit4TestAdapter(org.alfresco.repo.search.impl.solr.SolrStoreMappingWrapperTest.class));
suite.addTest(new JUnit4TestAdapter(org.alfresco.repo.security.authentication.CompositePasswordEncoderTest.class));
suite.addTest(new JUnit4TestAdapter(org.alfresco.repo.security.authentication.RepositoryAuthenticationDaoHashingTest.class));
suite.addTest(org.alfresco.traitextender.TraitExtenderUnitTestSuite.suite()); suite.addTest(org.alfresco.traitextender.TraitExtenderUnitTestSuite.suite());
suite.addTest(org.alfresco.repo.virtual.VirtualizationUnitTestSuite.suite()); } suite.addTest(org.alfresco.repo.virtual.VirtualizationUnitTestSuite.suite()); }

View File

@@ -89,8 +89,7 @@ public class AuthenticationTest extends TestCase
private AuthorityService authorityService; private AuthorityService authorityService;
private TenantService tenantService; private TenantService tenantService;
private TenantAdminService tenantAdminService; private TenantAdminService tenantAdminService;
private MD4PasswordEncoder passwordEncoder; private CompositePasswordEncoder compositePasswordEncoder;
private PasswordEncoder sha256PasswordEncoder;
private MutableAuthenticationDao dao; private MutableAuthenticationDao dao;
private AuthenticationManager authenticationManager; private AuthenticationManager authenticationManager;
private TicketComponent ticketComponent; private TicketComponent ticketComponent;
@@ -148,8 +147,7 @@ public class AuthenticationTest extends TestCase
authorityService = (AuthorityService) ctx.getBean("authorityService"); authorityService = (AuthorityService) ctx.getBean("authorityService");
tenantService = (TenantService) ctx.getBean("tenantService"); tenantService = (TenantService) ctx.getBean("tenantService");
tenantAdminService = (TenantAdminService) ctx.getBean("tenantAdminService"); tenantAdminService = (TenantAdminService) ctx.getBean("tenantAdminService");
passwordEncoder = (MD4PasswordEncoder) ctx.getBean("passwordEncoder"); compositePasswordEncoder = (CompositePasswordEncoder) ctx.getBean("compositePasswordEncoder");
sha256PasswordEncoder = (PasswordEncoder) ctx.getBean("sha256PasswordEncoder");
ticketComponent = (TicketComponent) ctx.getBean("ticketComponent"); ticketComponent = (TicketComponent) ctx.getBean("ticketComponent");
authenticationService = (MutableAuthenticationService) ctx.getBean("authenticationService"); authenticationService = (MutableAuthenticationService) ctx.getBean("authenticationService");
pubAuthenticationService = (MutableAuthenticationService) ctx.getBean("AuthenticationService"); pubAuthenticationService = (MutableAuthenticationService) ctx.getBean("AuthenticationService");
@@ -228,8 +226,7 @@ public class AuthenticationTest extends TestCase
dao.setTenantService(tenantService); dao.setTenantService(tenantService);
dao.setNodeService(nodeService); dao.setNodeService(nodeService);
dao.setNamespaceService(getNamespacePrefixReolsver("")); dao.setNamespaceService(getNamespacePrefixReolsver(""));
dao.setPasswordEncoder(passwordEncoder); dao.setCompositePasswordEncoder(compositePasswordEncoder);
dao.setSha256PasswordEncoder(sha256PasswordEncoder);
dao.setPolicyComponent(policyComponent); dao.setPolicyComponent(policyComponent);
dao.setAuthenticationCache(authenticationCache); dao.setAuthenticationCache(authenticationCache);
dao.setSingletonCache(immutableSingletonCache); dao.setSingletonCache(immutableSingletonCache);
@@ -449,8 +446,7 @@ public class AuthenticationTest extends TestCase
dao.setNodeService(nodeService); dao.setNodeService(nodeService);
dao.setAuthorityService(authorityService); dao.setAuthorityService(authorityService);
dao.setNamespaceService(getNamespacePrefixReolsver("")); dao.setNamespaceService(getNamespacePrefixReolsver(""));
dao.setPasswordEncoder(passwordEncoder); dao.setCompositePasswordEncoder(compositePasswordEncoder);
dao.setSha256PasswordEncoder(sha256PasswordEncoder);
dao.setPolicyComponent(policyComponent); dao.setPolicyComponent(policyComponent);
dao.setAuthenticationCache(authenticationCache); dao.setAuthenticationCache(authenticationCache);
dao.setSingletonCache(immutableSingletonCache); dao.setSingletonCache(immutableSingletonCache);
@@ -495,10 +491,6 @@ public class AuthenticationTest extends TestCase
dao.createUser("Andy", "cabbage".toCharArray()); dao.createUser("Andy", "cabbage".toCharArray());
assertNotNull(dao.getUserOrNull("Andy")); assertNotNull(dao.getUserOrNull("Andy"));
byte[] decodedHash = passwordEncoder.decodeHash(dao.getMD4HashedPassword("Andy"));
byte[] testHash = MessageDigest.getInstance("MD4").digest("cabbage".getBytes("UnicodeLittleUnmarked"));
assertEquals(new String(decodedHash), new String(testHash));
UserDetails AndyDetails = (UserDetails) dao.loadUserByUsername("Andy"); UserDetails AndyDetails = (UserDetails) dao.loadUserByUsername("Andy");
assertNotNull(AndyDetails); assertNotNull(AndyDetails);
assertEquals("Andy", AndyDetails.getUsername()); assertEquals("Andy", AndyDetails.getUsername());
@@ -508,7 +500,7 @@ public class AuthenticationTest extends TestCase
assertTrue(AndyDetails.isCredentialsNonExpired()); assertTrue(AndyDetails.isCredentialsNonExpired());
assertTrue(AndyDetails.isEnabled()); assertTrue(AndyDetails.isEnabled());
assertNotSame("cabbage", AndyDetails.getPassword()); assertNotSame("cabbage", AndyDetails.getPassword());
assertEquals(AndyDetails.getPassword(), passwordEncoder.encodePassword("cabbage", dao.getSalt(AndyDetails))); assertEquals(AndyDetails.getPassword(), compositePasswordEncoder.encodePreferred("cabbage", null));
assertEquals(1, AndyDetails.getAuthorities().length); assertEquals(1, AndyDetails.getAuthorities().length);
// Object oldSalt = dao.getSalt(AndyDetails); // Object oldSalt = dao.getSalt(AndyDetails);

View File

@@ -28,11 +28,11 @@ import static org.junit.Assert.fail;
import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.util.GUID; import org.alfresco.util.GUID;
import org.junit.Before; import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test; import org.junit.Test;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -43,20 +43,17 @@ import java.util.Map;
*/ */
public class CompositePasswordEncoderTest public class CompositePasswordEncoderTest
{ {
CompositePasswordEncoder encoder; CompositePasswordEncoder encoder;
static Map<String,Object> encodersConfig; public static Map<String,Object> encodersConfig;
private static final String SOURCE_PASSWORD = "SOURCE PASS Word $%%^ #';/,_+{+} like €this"+"\u4eca\u65e5\u306f\u4e16\u754c"; private static final String SOURCE_PASSWORD = "SOURCE PASS Word $%%^ #';/,_+{+} like €this"+"\u4eca\u65e5\u306f\u4e16\u754c";
@BeforeClass static {
public static void setUpClass() throws Exception
{
encodersConfig = new HashMap<>(); encodersConfig = new HashMap<>();
encodersConfig.put("md4", new MD4PasswordEncoderImpl()); encodersConfig.put("md4", new MD4PasswordEncoderImpl());
encodersConfig.put("sha256", new ShaPasswordEncoderImpl(256)); encodersConfig.put("sha256", new ShaPasswordEncoderImpl(256));
encodersConfig.put("bcrypt10",new BCryptPasswordEncoder(10)); encodersConfig.put("bcrypt10",new BCryptPasswordEncoder(10));
encodersConfig.put("bcrypt20",new BCryptPasswordEncoder(20)); encodersConfig.put("bcrypt11",new BCryptPasswordEncoder(11));
encodersConfig.put("bcrypt30",new BCryptPasswordEncoder(30)); encodersConfig.put("bcrypt12",new BCryptPasswordEncoder(11));
encodersConfig.put("badencoder",new Object()); encodersConfig.put("badencoder",new Object());
} }
@@ -251,6 +248,14 @@ public class CompositePasswordEncoderTest
assertTrue(encoder.matchesPassword(SOURCE_PASSWORD, encoded, salt, mdbChain)); assertTrue(encoder.matchesPassword(SOURCE_PASSWORD, encoded, salt, mdbChain));
} }
@Test
public void testEncodePreferred() throws Exception
{
encoder.setPreferredEncoding("bcrypt10");
String encoded = encoder.encodePreferred(SOURCE_PASSWORD, null);
assertTrue(encoder.matches("bcrypt10", SOURCE_PASSWORD, encoded, null));
}
@Test @Test
public void testMandatoryProperties() throws Exception public void testMandatoryProperties() throws Exception
{ {
@@ -264,8 +269,6 @@ public class CompositePasswordEncoderTest
} }
subject.setEncoders(encodersConfig); subject.setEncoders(encodersConfig);
subject.setLegacyEncoding("md345");
subject.setLegacyEncodingProperty("password");
try try
{ {
@@ -280,8 +283,6 @@ public class CompositePasswordEncoderTest
subject.init(); subject.init();
assertEquals("nice_encoding", subject.getPreferredEncoding()); assertEquals("nice_encoding", subject.getPreferredEncoding());
assertEquals("md345", subject.getLegacyEncoding());
assertEquals("password", subject.getLegacyEncodingProperty());
} }
@Test @Test
@@ -289,7 +290,15 @@ public class CompositePasswordEncoderTest
{ {
CompositePasswordEncoder subject = new CompositePasswordEncoder(); CompositePasswordEncoder subject = new CompositePasswordEncoder();
subject.setPreferredEncoding("fish"); subject.setPreferredEncoding("fish");
assertTrue(subject.isPreferredEncoding("fish")); assertTrue(subject.lastEncodingIsPreferred(Arrays.asList("fish")));
assertEquals("fish", subject.getPreferredEncoding()); assertEquals("fish", subject.getPreferredEncoding());
assertFalse(subject.lastEncodingIsPreferred((List)null));
assertFalse(subject.lastEncodingIsPreferred(Collections.<String>emptyList()));
assertTrue(subject.lastEncodingIsPreferred(Arrays.asList("fish")));
assertFalse(subject.lastEncodingIsPreferred(Arrays.asList("bird")));
assertTrue(subject.lastEncodingIsPreferred(Arrays.asList("bird", "fish")));
assertFalse(subject.lastEncodingIsPreferred(Arrays.asList("bird", "fish", "dog", "cat")));
assertTrue(subject.lastEncodingIsPreferred(Arrays.asList("bird", "dog", "cat","fish")));
} }
} }

View File

@@ -0,0 +1,177 @@
package org.alfresco.repo.security.authentication;
import static org.junit.Assert.*;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ContentModel;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.GUID;
import org.alfresco.util.Pair;
import org.junit.Before;
import org.junit.Test;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Created by gethin on 01/10/15.
*/
public class RepositoryAuthenticationDaoHashingTest
{
RepositoryAuthenticationDao authenticationDao;
CompositePasswordEncoder cpe;
@Before
public void setUp() throws Exception
{
authenticationDao = new RepositoryAuthenticationDao();
cpe = new CompositePasswordEncoder();
cpe.setEncoders(CompositePasswordEncoderTest.encodersConfig);
authenticationDao.setCompositePasswordEncoder(cpe);
}
@Test
public void testRehashedPassword() throws Exception
{
//Use md4
cpe.setPreferredEncoding("md4");
String salt = GUID.generate();
String md4Hashed = cpe.encode("md4","HASHED_MY_PASSWORD", null);
String sha256Hashed = cpe.encode("sha256","HASHED_MY_PASSWORD", salt);
Map<QName, Serializable> properties = new HashMap<>();
properties.put(ContentModel.PROP_PASSWORD, "nonsense");
assertFalse("Should be empty", properties.containsKey(ContentModel.PROP_PASSWORD_HASH));
//No hashing to do but we need to update the Indicator
assertTrue(authenticationDao.rehashedPassword(properties));
assertEquals(CompositePasswordEncoder.MD4, properties.get(ContentModel.PROP_HASH_INDICATOR));
assertTrue("Should now contain the password", properties.containsKey(ContentModel.PROP_PASSWORD_HASH));
assertFalse("Should remove the property", properties.containsKey(ContentModel.PROP_PASSWORD));
assertFalse("Should remove the property", properties.containsKey(ContentModel.PROP_PASSWORD_SHA256));
assertEquals("nonsense", properties.get(ContentModel.PROP_PASSWORD_HASH));
//We copied the plain text (above) but it won't work (see next)
properties.clear();
properties.put(ContentModel.PROP_PASSWORD, "PLAIN TEXT PASSWORD");
//We don't support plain text.
assertTrue(authenticationDao.rehashedPassword(properties));
assertEquals(CompositePasswordEncoder.MD4, properties.get(ContentModel.PROP_HASH_INDICATOR));
assertTrue("Should now contain the password", properties.containsKey(ContentModel.PROP_PASSWORD_HASH));
assertFalse("Should remove the property", properties.containsKey(ContentModel.PROP_PASSWORD));
assertFalse("Should remove the property", properties.containsKey(ContentModel.PROP_PASSWORD_SHA256));
assertEquals("PLAIN TEXT PASSWORD", properties.get(ContentModel.PROP_PASSWORD_HASH));
assertFalse("We copied a plain text password to the new property but"
+" the legacy encoding is set to MD4 so the password would NEVER match.",
matches("PLAIN TEXT PASSWORD", properties, cpe));
properties.clear();
properties.put(ContentModel.PROP_PASSWORD, md4Hashed);
cpe.setPreferredEncoding("bcrypt10");
assertTrue("We have the property", properties.containsKey(ContentModel.PROP_PASSWORD));
assertFalse("Should be empty", properties.containsKey(ContentModel.PROP_PASSWORD_HASH));
//We rehashed this password by taking the md4 and hashing it by bcrypt
assertTrue(authenticationDao.rehashedPassword(properties));
assertEquals(Arrays.asList("md4","bcrypt10"), properties.get(ContentModel.PROP_HASH_INDICATOR));
assertTrue("Should now contain the password", properties.containsKey(ContentModel.PROP_PASSWORD_HASH));
assertTrue(matches("HASHED_MY_PASSWORD", properties, cpe));
assertFalse("Should remove the property", properties.containsKey(ContentModel.PROP_PASSWORD));
assertFalse("Should remove the property", properties.containsKey(ContentModel.PROP_PASSWORD_SHA256));
properties.clear();
properties.put(ContentModel.PROP_PASSWORD, "This should be ignored");
properties.put(ContentModel.PROP_PASSWORD_SHA256, sha256Hashed);
properties.put(ContentModel.PROP_SALT, salt);
assertTrue("We have the property", properties.containsKey(ContentModel.PROP_PASSWORD));
assertTrue("We have the property", properties.containsKey(ContentModel.PROP_PASSWORD_SHA256));
assertFalse("Should be empty", properties.containsKey(ContentModel.PROP_PASSWORD_HASH));
//We rehashed this password by taking the sha256 and hashing it by bcrypt
assertTrue(authenticationDao.rehashedPassword(properties));
assertEquals(Arrays.asList("sha256","bcrypt10"), properties.get(ContentModel.PROP_HASH_INDICATOR));
assertTrue("Should now contain the password", properties.containsKey(ContentModel.PROP_PASSWORD_HASH));
assertTrue(matches("HASHED_MY_PASSWORD", properties, cpe));
assertFalse("Should remove the property", properties.containsKey(ContentModel.PROP_PASSWORD));
assertFalse("Should remove the property", properties.containsKey(ContentModel.PROP_PASSWORD_SHA256));
}
@Test
public void testRehashedPasswordBcrypt() throws Exception
{
cpe.setPreferredEncoding("bcrypt10");
Map<QName, Serializable> properties = new HashMap<>();
properties.put(ContentModel.PROP_HASH_INDICATOR, (Serializable) Arrays.asList("bcrypt10"));
properties.put(ContentModel.PROP_PASSWORD_HASH, "long hash");
//Nothing to do.
assertFalse(authenticationDao.rehashedPassword(properties));
cpe.setPreferredEncoding("bcrypt11");
assertTrue(authenticationDao.rehashedPassword(properties));
assertEquals(Arrays.asList("bcrypt10","bcrypt11"),authenticationDao.determinePasswordHash(properties).getFirst());
cpe.setPreferredEncoding("bcrypt12");
assertTrue(authenticationDao.rehashedPassword(properties));
//Triple hashing!
assertEquals(Arrays.asList("bcrypt10","bcrypt11","bcrypt12"),authenticationDao.determinePasswordHash(properties).getFirst());
}
@Test
public void testGetPasswordHash() throws Exception
{
Map<QName, Serializable> properties = new HashMap<>();
cpe.setPreferredEncoding("bcrypt10");
try
{
authenticationDao.determinePasswordHash(properties);
fail("Should throw exception");
}
catch (AlfrescoRuntimeException are)
{
assertTrue(are.getMessage().contains("Unable to find a user password"));
}
//if the PROP_PASSWORD field is the only one availble then we are using MD4
properties.put(ContentModel.PROP_PASSWORD, "mypassword");
Pair<List<String>, String> passwordHashed = authenticationDao.determinePasswordHash(properties);
assertEquals(CompositePasswordEncoder.MD4, passwordHashed.getFirst());
assertEquals("mypassword", passwordHashed.getSecond());
//if the PROP_PASSWORD_SHA256 field is used then we are using SHA256
properties.put(ContentModel.PROP_PASSWORD_SHA256, "sha_password");
passwordHashed = authenticationDao.determinePasswordHash(properties);
assertEquals(CompositePasswordEncoder.SHA256, passwordHashed.getFirst());
assertEquals("sha_password", passwordHashed.getSecond());
properties.put(ContentModel.PROP_HASH_INDICATOR, null);
//If the indicator is NULL then it still uses the old password field
passwordHashed = authenticationDao.determinePasswordHash(properties);
assertEquals(CompositePasswordEncoder.SHA256, passwordHashed.getFirst());
assertEquals("sha_password", passwordHashed.getSecond());
properties.put(ContentModel.PROP_HASH_INDICATOR, new ArrayList<String>(0));
//If the indicator doesn't have a value
passwordHashed = authenticationDao.determinePasswordHash(properties);
assertEquals(CompositePasswordEncoder.SHA256, passwordHashed.getFirst());
assertEquals("sha_password", passwordHashed.getSecond());
//Now it uses the correct property
properties.put(ContentModel.PROP_HASH_INDICATOR, (Serializable) Arrays.asList("myencoding"));
properties.put(ContentModel.PROP_PASSWORD_HASH, "hashed this time");
passwordHashed = authenticationDao.determinePasswordHash(properties);
assertEquals(Arrays.asList("myencoding"), passwordHashed.getFirst());
assertEquals("hashed this time",passwordHashed.getSecond());
}
private static boolean matches(String password, Map<QName, Serializable> properties, CompositePasswordEncoder cpe)
{
return cpe.matchesPassword(password, (String) properties.get(ContentModel.PROP_PASSWORD_HASH), (String) properties.get(ContentModel.PROP_SALT), (List<String>) properties.get(ContentModel.PROP_HASH_INDICATOR) );
}
}