dhis2-devs team mailing list archive
-
dhis2-devs team
-
Mailing list archive
-
Message #33133
[Branch ~dhis2-devs-core/dhis2/trunk] Rev 16881: Merge from Halvdan Hoem Grelland / lp:~halvdanhg/dhis2/bcrypt-password-hash-migration. Implements...
Merge authors:
Halvdan Hoem Grelland (halvdanhg)
------------------------------------------------------------
revno: 16881 [merge]
committer: Lars Helge Overland <larshelge@xxxxxxxxx>
branch nick: dhis2
timestamp: Tue 2014-09-30 15:48:45 +0200
message:
Merge from Halvdan Hoem Grelland / lp:~halvdanhg/dhis2/bcrypt-password-hash-migration. Implements bcrypt hashing of passwords which radically improves password security.
added:
dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/migration/
dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/migration/MigrationAuthenticationProvider.java
dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/migration/MigrationPasswordManager.java
dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/migration/MigrationSpringSecurityPasswordManager.java
dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/security/PasswordManagerTest.java
modified:
dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/UserCredentials.java
dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/DefaultSecurityService.java
dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/PasswordManager.java
dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/spring/SpringSecurityPasswordManager.java
dhis-2/dhis-services/dhis-service-core/src/main/resources/META-INF/dhis/security.xml
dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/user/hibernate/UserCredentials.hbm.xml
dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/security/SecurityServiceTest.java
dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/translation/TranslationServiceTest.java
dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/DhisSpringTest.java
dhis-2/dhis-web/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AccountController.java
dhis-2/dhis-web/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/user/UserController.java
dhis-2/dhis-web/dhis-web-commons/src/main/java/org/hisp/dhis/security/DatabaseAutomaticAccessProvider.java
dhis-2/dhis-web/dhis-web-commons/src/main/java/org/hisp/dhis/useraccount/action/UpdateUserAccountAction.java
dhis-2/dhis-web/dhis-web-commons/src/main/resources/META-INF/dhis/security.xml
dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/java/org/hisp/dhis/user/action/AddUserAction.java
dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/java/org/hisp/dhis/user/action/DeleteCurrentUserAction.java
dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/java/org/hisp/dhis/user/action/UpdateUserAction.java
--
lp:dhis2
https://code.launchpad.net/~dhis2-devs-core/dhis2/trunk
Your team DHIS 2 developers is subscribed to branch lp:dhis2.
To unsubscribe from this branch go to https://code.launchpad.net/~dhis2-devs-core/dhis2/trunk/+edit-subscription
=== modified file 'dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/UserCredentials.java'
--- dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/UserCredentials.java 2014-09-24 09:23:28 +0000
+++ dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/UserCredentials.java 2014-09-30 12:29:45 +0000
@@ -342,73 +342,28 @@
}
/**
- * Tests whether the given input arguments can perform a valid restore of
- * the user account for these credentials.
- * <p>
- * If fail, returns one of the following error strings:
- * <ul>
- * <li>account_restoreToken_is_null</li>
- * <li>account_restoreCode_is_null</li>
- * <li>account_restoreExpiry_is_null</li>
- * <li>token_parameter_is_null</li>
- * <li>code_parameter_is_null</li>
- * <li>date_parameter_is_null</li>
- * <li>token_does_not_match_restoreToken ...</li>
- * <li>code_does_not_match_restoreCode ...</li>
- * <li>date_is_after_expiry ...</li>
- * </ul>
- * @param token the restore token.
- * @param code the restore code.
- * @param date the expiry date.
- * @return null if success, or error message if fail.
+ * Tests whether the credentials contain all needed parameters to
+ * perform an account restore.
+ * If a parameter is missing a descriptive error string is returned.
+ * @return null on success, a descriptive error string on failure.
*/
- public String canRestore( String token, String code, Date date )
+ public String isRestorable()
{
- if ( this.restoreToken == null )
+ if ( restoreToken == null )
{
return "account_restoreToken_is_null";
}
- if ( this.restoreCode == null )
+ if ( restoreCode == null )
{
return "account_restoreCode_is_null";
}
- if ( this.restoreExpiry == null )
+ if ( restoreExpiry == null )
{
return "account_restoreExpiry_is_null";
}
- if ( token == null )
- {
- return "token_parameter_is_null";
- }
-
- if ( code == null )
- {
- return "code_parameter_is_null";
- }
-
- if ( date == null )
- {
- return "date_parameter_is_null";
- }
-
- if ( !token.equals ( this.restoreToken ) )
- {
- return ( "token_does_not_match_restoreToken - token: '" + token + "' restoreToken: '" + restoreToken + "'" );
- }
-
- if ( !code.equals ( this.restoreCode ) )
- {
- return ( "code_does_not_match_restoreCode - code: '" + code + "' restoreCode: '" + restoreCode + "'" );
- }
-
- if ( date.after( this.restoreExpiry ) )
- {
- return "date_is_after_expiry - date: " + date.toString() + " expiry: " + this.restoreExpiry.toString();
- }
-
return null; // Success.
}
=== modified file 'dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/DefaultSecurityService.java'
--- dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/DefaultSecurityService.java 2014-09-25 07:29:20 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/DefaultSecurityService.java 2014-09-30 13:03:38 +0000
@@ -140,7 +140,7 @@
user.setSurname( "(TBD)" );
user.setFirstName( "(TBD)" );
- user.getUserCredentials().setPassword( passwordManager.encodePassword( user.getUsername(), rawPassword ) );
+ user.getUserCredentials().setPassword( passwordManager.encodePassword( rawPassword ) );
return true;
}
@@ -223,8 +223,8 @@
String token = restoreOptions.getTokenPrefix() + CodeGenerator.generateCode( RESTORE_TOKEN_LENGTH );
String code = CodeGenerator.generateCode( RESTORE_CODE_LENGTH );
- String hashedToken = passwordManager.encodePassword( credentials.getUsername(), token );
- String hashedCode = passwordManager.encodePassword( credentials.getUsername(), code );
+ String hashedToken = passwordManager.encodePassword( token );
+ String hashedCode = passwordManager.encodePassword( code );
RestoreType restoreType = restoreOptions.getRestoreType();
@@ -236,8 +236,7 @@
userService.updateUserCredentials( credentials );
- String[] result = { token, code };
- return result;
+ return new String[] { token, code };
}
public RestoreOptions getRestoreOptions( String token )
@@ -253,9 +252,7 @@
return false;
}
- String username = credentials.getUsername();
-
- newPassword = passwordManager.encodePassword( username, newPassword );
+ newPassword = passwordManager.encodePassword( newPassword );
credentials.setPassword( newPassword );
@@ -270,28 +267,13 @@
public boolean canRestore( UserCredentials credentials, String token, String code, RestoreType restoreType )
{
- String logPrefix = "Restore user: " + credentials.getUid() + ", username: " + credentials.getUsername();
-
- String errorMessage = verifyToken( credentials, token, restoreType );
-
- if ( errorMessage != null )
- {
- log.info( logPrefix + " verifyToken() failed: " + errorMessage );
- return false;
- }
-
- String username = credentials.getUsername();
-
- String encodedToken = passwordManager.encodePassword( username, token );
- String encodedCode = passwordManager.encodePassword( username, code );
-
- Date date = new Cal().now().time();
-
- errorMessage = credentials.canRestore( encodedToken, encodedCode, date );
-
- if ( errorMessage != null )
- {
- log.info( logPrefix + " canRestore() failed: " + errorMessage );
+ String logPrefix = "Restore user: " + credentials.getUid() + ", username: " + credentials.getUsername() + " ";
+
+ String errorMessage = verifyRestore( credentials, token, code, restoreType );
+
+ if ( errorMessage != null )
+ {
+ log.info( logPrefix + "Failed to verify restore: " + errorMessage );
return false;
}
@@ -300,6 +282,76 @@
}
/**
+ * Verifies all parameters needed for account restore and checks validity of the
+ * user supplied token and code. If the restore cannot be verified a descriptive
+ * error string is returned.
+ * @param credentials the user credentials.
+ * @param token the user supplied token.
+ * @param code the user supplied code.
+ * @param restoreType the restore type.
+ * @return null if restore is valid, a descriptive error string otherwise.
+ */
+ private String verifyRestore( UserCredentials credentials, String token, String code, RestoreType restoreType )
+ {
+ String errorMessage = credentials.isRestorable();
+
+ if ( errorMessage != null )
+ {
+ return errorMessage;
+ }
+
+ errorMessage = verifyToken( credentials, token, restoreType );
+
+ if ( errorMessage != null )
+ {
+ return errorMessage;
+ }
+
+ errorMessage = verifyRestoreCode( credentials, code );
+
+ if ( errorMessage != null )
+ {
+ return errorMessage;
+ }
+
+ Date currentTime = new Cal().now().time();
+ Date restoreExpiry = credentials.getRestoreExpiry();
+
+ if ( currentTime.after( restoreExpiry ) )
+ {
+ return "date_is_after_expiry - date: " + currentTime.toString() + " expiry: " + restoreExpiry.toString();
+ }
+
+ return null; // Success;
+ }
+
+ /**
+ * Verifies a user supplied restore code against the stored restore code.
+ * If the code cannot be verified a descriptive error string is returned.
+ * @param credentials the user credentials.
+ * @param code the user supplied code.
+ * @return null on success, a descriptive error string otherwise.
+ */
+ private String verifyRestoreCode( UserCredentials credentials, String code )
+ {
+ String restoreCode = credentials.getRestoreCode();
+
+ if( code == null )
+ {
+ return "code_parameter_is_null";
+ }
+
+ if ( restoreCode == null )
+ {
+ return "account_restoreCode_is_null";
+ }
+
+ boolean validCode = passwordManager.matches( code, restoreCode );
+
+ return validCode ? null : "code_does_not_match_restoreCode - code: '"+ code + "' restoreCode: '" + restoreCode + "'" ;
+ }
+
+ /**
* Verify the token given for a user invite or password restore operation.
* <p>
* If error, returns one of the following strings:
@@ -308,10 +360,10 @@
* <li>credentials_parameter_is_null</li>
* <li>token_parameter_is_null</li>
* <li>restore_type_parameter_is_null</li>
- * <li>cannnot_parse_restore_options ...</li>
+ * <li>cannot_parse_restore_options ...</li>
* <li>wrong_prefix_for_restore_type ...</li>
* <li>could_not_verify_token ...</li>
- * <li>restore_token_does_not_match_supplied_token</li>
+ * <li>restore_token_does_not_match_supplied_token ...</li>
* </ul>
*
* @param credentials the user credentials.
@@ -340,7 +392,7 @@
if ( restoreOptions == null )
{
- return "cannnot_parse_restore_options for " + restoreType.name() + " from token " + token;
+ return "cannot_parse_restore_options for " + restoreType.name() + " from token " + token;
}
if ( restoreType != restoreOptions.getRestoreType() )
@@ -348,19 +400,16 @@
return "wrong_prefix_for_restore_type " + restoreType.name() + " on token " + token;
}
- if ( credentials.getRestoreToken() == null )
+ String restoreToken = credentials.getRestoreToken();
+
+ if ( restoreToken == null )
{
return "could_not_verify_token for " + restoreType.name() + " because user has no token";
}
- String encodedToken = passwordManager.encodePassword( credentials.getUsername(), token );
-
- if ( !credentials.getRestoreToken().equals( encodedToken ) )
- {
- return "restore_token_does_not_match_supplied_token " + token;
- }
-
- return null;
+ boolean validToken = passwordManager.matches( token, restoreToken );
+
+ return validToken ? null : "restore_token_does_not_match_supplied_token " + token;
}
@Override
=== modified file 'dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/PasswordManager.java'
--- dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/PasswordManager.java 2014-03-18 08:10:10 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/PasswordManager.java 2014-08-25 15:29:21 +0000
@@ -30,11 +30,34 @@
/**
* @author Torgeir Lorange Ostby
- * @version $Id: PasswordManager.java 3109 2007-03-19 17:05:21Z torgeilo $
+ * @author Halvdan Hoem Grelland
*/
public interface PasswordManager
{
String ID = PasswordManager.class.getName();
- String encodePassword( String username, String password );
+ /**
+ * Cryptographically hash a password.
+ * Salting (as well as the salt storage scheme) must be handled by the implementation.
+ *
+ * @param password password to encode.
+ * @return the hashed password.
+ */
+ String encodePassword( String password );
+
+ /**
+ * Determines whether the supplied password equals the encoded password or not.
+ * Fetching and handling of any required salt value must be done by the implementation.
+ *
+ * @param rawPassword the raw, unencoded password.
+ * @param encodedPassword the encoded password to match against.
+ * @return true if the passwords match, false otherwise.
+ */
+ boolean matches( String rawPassword, String encodedPassword );
+
+ /**
+ * Returns the class name of the password encoder used by this instance.
+ * @return the name of the password encoder class.
+ */
+ String getPasswordEncoderClassName();
}
=== added directory 'dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/migration'
=== added file 'dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/migration/MigrationAuthenticationProvider.java'
--- dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/migration/MigrationAuthenticationProvider.java 1970-01-01 00:00:00 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/migration/MigrationAuthenticationProvider.java 2014-08-26 12:00:27 +0000
@@ -0,0 +1,85 @@
+package org.hisp.dhis.security.migration;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.hisp.dhis.user.UserCredentials;
+import org.hisp.dhis.user.UserService;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.util.Date;
+
+/**
+ * Implements migration of legacy user password hashes on user login.
+ *
+ * The procedure to do so works by preceding the ordinary authentication check
+ * (which is performed using the current password hashing method) with an authentication
+ * procedure using the legacy password hashing method.
+ *
+ * If the currently stored hash and the legacyHash(suppliedPassword, usernameSalt) matches
+ * the password is hashed again using the current method and replaces the stored hash for the user.
+ * The user is now migrated to the current password hashing scheme and will on next logon not
+ * authenticate using the legacy hash method but the current one.
+ *
+ * In either case the call is followed by the authentication procedure in DaoAuthenticationProvider
+ * which performs the final authentication (using the current method).
+ *
+ * @author Halvdan Hoem Grelland
+ */
+public class MigrationAuthenticationProvider
+ extends DaoAuthenticationProvider
+{
+ private static final Log log = LogFactory.getLog( MigrationAuthenticationProvider.class );
+
+ // -------------------------------------------------------------------------
+ // Dependencies
+ // -------------------------------------------------------------------------
+
+ private UserService userService;
+
+ public void setUserService( UserService userService )
+ {
+ this.userService = userService;
+ }
+
+ private MigrationPasswordManager passwordManager;
+
+ public void setPasswordManager( MigrationPasswordManager passwordManager )
+ {
+ this.passwordManager = passwordManager;
+ }
+
+ // -------------------------------------------------------------------------
+ // Pre-auth check-and-switch for legacy password hash match
+ // -------------------------------------------------------------------------
+
+ @Override
+ protected void additionalAuthenticationChecks( UserDetails userDetails,
+ UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken )
+ throws AuthenticationException
+ {
+ String password = (String) usernamePasswordAuthenticationToken.getCredentials();
+ String username = userDetails.getUsername();
+
+ // If legacyHash(password, username) matches stored hash, re-hash password with current method and switch with stored hash
+ if( passwordManager.legacyMatches( userDetails.getPassword(), password, username ) )
+ {
+ UserCredentials userCredentials = userService.getUserCredentialsByUsername( username );
+
+ if ( userCredentials != null )
+ {
+ userCredentials.setPassword( passwordManager.encodePassword( password ) );
+ userCredentials.setPasswordLastUpdated( new Date() );
+ userService.updateUser( userCredentials.getUser() );
+
+ log.info( "User " + userCredentials.getUsername() + " was migrated from " + passwordManager.getLegacyPasswordEncoderClassName() +
+ " to " + passwordManager.getPasswordEncoderClassName() + " based password hashing on login." );
+
+ userDetails = getUserDetailsService().loadUserByUsername( username );
+ }
+ }
+ super.additionalAuthenticationChecks( userDetails, usernamePasswordAuthenticationToken );
+ }
+}
=== added file 'dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/migration/MigrationPasswordManager.java'
--- dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/migration/MigrationPasswordManager.java 1970-01-01 00:00:00 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/migration/MigrationPasswordManager.java 2014-08-26 12:00:27 +0000
@@ -0,0 +1,41 @@
+package org.hisp.dhis.security.migration;
+
+import org.hisp.dhis.security.PasswordManager;
+
+/**
+ * Drop-in replacement for PasswordManager which provides access to legacy password hashing methods as
+ * well as the currently used hashing methods. This is useful in implementing seamless migration to
+ * a new and more secure password hashing method. In such a migration phase the system will need to
+ * be able to accept login requests from users whose passwords are stored using legacy hash method
+ * in order to re-hash and store the user password hash using the new (current) method (handled elsewhere).
+ *
+ * @author Halvdan Hoem Grelland
+ */
+public interface MigrationPasswordManager
+ extends PasswordManager
+{
+ /**
+ * Cryptographically hash a password using a legacy method.
+ * Useful for access to the former (legacy) hash method when implementing migration to a new method.
+ * @param password the password to encode.
+ * @param username the username (used for seeding salt generation).
+ * @return the encoded (hashed) password.
+ */
+ public String legacyEncodePassword( String password, String username );
+
+ /**
+ * Determines whether the supplied password equals the encoded password or not.
+ * Uses the legacy hashing method to do so and is useful in implementing migration to a new method.
+ * @param encodedPassword the encoded password.
+ * @param password the password to match.
+ * @param username the username (used for salt generation).
+ * @return true if the password matches the encoded password, false otherwise.
+ */
+ public boolean legacyMatches( String encodedPassword, String password, String username );
+
+ /**
+ * Return the class name of the legacy password encoder.
+ * @return the name of the legacy password encoder class.
+ */
+ public String getLegacyPasswordEncoderClassName();
+}
=== added file 'dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/migration/MigrationSpringSecurityPasswordManager.java'
--- dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/migration/MigrationSpringSecurityPasswordManager.java 1970-01-01 00:00:00 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/migration/MigrationSpringSecurityPasswordManager.java 2014-08-27 13:26:08 +0000
@@ -0,0 +1,56 @@
+package org.hisp.dhis.security.migration;
+
+import org.hisp.dhis.security.UsernameSaltSource;
+import org.hisp.dhis.security.spring.SpringSecurityPasswordManager;
+import org.springframework.security.authentication.encoding.PasswordEncoder;
+
+/**
+ * @author Halvdan Hoem Grelland
+ */
+public class MigrationSpringSecurityPasswordManager
+ extends SpringSecurityPasswordManager
+ implements MigrationPasswordManager
+{
+ public static String legacyPasswordEncoderClassName;
+
+ // -------------------------------------------------------------------------
+ // Dependencies
+ // -------------------------------------------------------------------------
+
+ private PasswordEncoder legacyPasswordEncoder;
+
+ public void setLegacyPasswordEncoder( PasswordEncoder legacyPasswordEncoder )
+ {
+ this.legacyPasswordEncoder = legacyPasswordEncoder;
+ legacyPasswordEncoderClassName = legacyPasswordEncoder.getClass().getName();
+ }
+
+ private UsernameSaltSource usernameSaltSource;
+
+ public void setUsernameSaltSource( UsernameSaltSource usernameSaltSource )
+ {
+ this.usernameSaltSource = usernameSaltSource;
+ }
+
+ // -------------------------------------------------------------------------
+ // MigrationPasswordManager implementation
+ // -------------------------------------------------------------------------
+
+ @Override
+ public String legacyEncodePassword( String password, String username )
+ {
+ return legacyPasswordEncoder.encodePassword( password, usernameSaltSource.getSalt( username ) );
+ }
+
+ @Override
+ public boolean legacyMatches( String encodedPassword, String password, String username )
+ {
+ return legacyPasswordEncoder.isPasswordValid( encodedPassword, password, usernameSaltSource.getSalt( username ) );
+ }
+
+ @Override
+ public String getLegacyPasswordEncoderClassName()
+ {
+ return legacyPasswordEncoder.getClass().getName();
+ }
+}
=== modified file 'dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/spring/SpringSecurityPasswordManager.java'
--- dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/spring/SpringSecurityPasswordManager.java 2014-03-18 08:10:10 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/spring/SpringSecurityPasswordManager.java 2014-08-27 13:26:08 +0000
@@ -29,12 +29,11 @@
*/
import org.hisp.dhis.security.PasswordManager;
-import org.hisp.dhis.security.UsernameSaltSource;
-import org.springframework.security.authentication.encoding.Md5PasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @author Torgeir Lorange Ostby
- * @version $Id: AcegiPasswordManager.java 3109 2007-03-19 17:05:21Z torgeilo $
+ * @author Halvdan Hoem Grelland
*/
public class SpringSecurityPasswordManager
implements PasswordManager
@@ -43,26 +42,32 @@
// Dependencies
// -------------------------------------------------------------------------
- private Md5PasswordEncoder passwordEncoder;
+ private PasswordEncoder passwordEncoder;
- public void setPasswordEncoder( Md5PasswordEncoder passwordEncoder )
+ public void setPasswordEncoder( PasswordEncoder passwordEncoder )
{
this.passwordEncoder = passwordEncoder;
}
- private UsernameSaltSource usernameSaltSource;
-
- public void setUsernameSaltSource( UsernameSaltSource saltSource )
- {
- this.usernameSaltSource = saltSource;
- }
-
// -------------------------------------------------------------------------
// PasswordManager implementation
// -------------------------------------------------------------------------
- public final String encodePassword( String username, String password )
- {
- return passwordEncoder.encodePassword( password, usernameSaltSource.getSalt( username ) );
+ @Override
+ public final String encodePassword( String password )
+ {
+ return passwordEncoder.encode( password );
+ }
+
+ @Override
+ public boolean matches( String rawPassword, String encodedPassword )
+ {
+ return passwordEncoder.matches( rawPassword, encodedPassword );
+ }
+
+ @Override
+ public String getPasswordEncoderClassName()
+ {
+ return passwordEncoder.getClass().getName();
}
}
=== modified file 'dhis-2/dhis-services/dhis-service-core/src/main/resources/META-INF/dhis/security.xml'
--- dhis-2/dhis-services/dhis-service-core/src/main/resources/META-INF/dhis/security.xml 2014-07-16 17:07:46 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/main/resources/META-INF/dhis/security.xml 2014-08-26 11:06:06 +0000
@@ -2,18 +2,50 @@
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd">
- <bean id="passwordEncoder" class="org.springframework.security.authentication.encoding.Md5PasswordEncoder" />
+ <bean id="md5PasswordEncoder" class="org.springframework.security.authentication.encoding.Md5PasswordEncoder" />
+
<bean id="usernameSaltSource" class="org.hisp.dhis.security.DefaultUsernameSaltSource" />
+ <bean id="bCryptPasswordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" />
+
<bean id="userDetailsService" class="org.hisp.dhis.security.DefaultUserDetailsService">
<property name="userService" ref="org.hisp.dhis.user.UserService" />
</bean>
+ <!--
+ As of version 2.17 the password hashing method has been switched from MD5 to bCrypt. In order to migrate user records
+ seamlessly to the new hashing scheme, the MigrationSpringSecurityPasswordManager and MigrationAuthenticationProvider should
+ be used in lieu of SpringSecurityPasswordManager and the default spring AuthenticationProvider respectively.
+
+ As soon as migration is deemed complete (in a later version, most likely 2.18 or 2.19) the org.hisp.dhis.security.migration
+ package components should be replaced.
+
+ The (working) configuration SpringSecurityPasswordManager as it should be used in later versions is shown
+ (commented out) below. The authentication provider will need no bean definition as the default configuration (with bCryptPasswordEncoder
+ chosen as the password-encoder) should be used.
+ -->
+
+ <!--
+ Replaced by MigrationSpringSecurityPasswordManager as long as the system is not fully migrated to bCrypt password hashes.
+
<bean id="org.hisp.dhis.security.PasswordManager" class="org.hisp.dhis.security.spring.SpringSecurityPasswordManager">
- <property name="passwordEncoder" ref="passwordEncoder" />
+ <property name="passwordEncoder" ref="bCryptPasswordEncoder" />
+ </bean>
+ -->
+
+ <bean id="org.hisp.dhis.security.PasswordManager" class="org.hisp.dhis.security.migration.MigrationSpringSecurityPasswordManager">
+ <property name="passwordEncoder" ref="bCryptPasswordEncoder" />
+ <property name="legacyPasswordEncoder" ref="md5PasswordEncoder" />
<property name="usernameSaltSource" ref="usernameSaltSource" />
</bean>
+ <bean id="migrationAuthenticationProvider" class="org.hisp.dhis.security.migration.MigrationAuthenticationProvider">
+ <property name="passwordManager" ref="org.hisp.dhis.security.PasswordManager" />
+ <property name="userService" ref="org.hisp.dhis.user.UserService" />
+ <property name="passwordEncoder" ref="bCryptPasswordEncoder" />
+ <property name="userDetailsService" ref="userDetailsService" />
+ </bean>
+
<bean id="org.hisp.dhis.security.SecurityService" class="org.hisp.dhis.security.DefaultSecurityService">
<property name="passwordManager" ref="org.hisp.dhis.security.PasswordManager" />
<property name="emailMessageSender" ref="emailMessageSender" />
=== modified file 'dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/user/hibernate/UserCredentials.hbm.xml'
--- dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/user/hibernate/UserCredentials.hbm.xml 2014-05-27 02:41:16 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/user/hibernate/UserCredentials.hbm.xml 2014-08-26 11:06:06 +0000
@@ -24,7 +24,7 @@
<column name="openid" unique="true" />
</property>
- <property name="password">
+ <property name="password" length="60">
<column name="password" not-null="false" />
</property>
=== added file 'dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/security/PasswordManagerTest.java'
--- dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/security/PasswordManagerTest.java 1970-01-01 00:00:00 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/security/PasswordManagerTest.java 2014-08-27 13:26:08 +0000
@@ -0,0 +1,36 @@
+package org.hisp.dhis.security;
+
+import org.hisp.dhis.DhisSpringTest;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import static junit.framework.TestCase.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests assume (and enforce) the passwordManager to use non-static salts.
+ * @author Halvdan Hoem Grelland
+ */
+public class PasswordManagerTest
+ extends DhisSpringTest
+{
+ @Autowired
+ private PasswordManager passwordManager;
+
+ @Test
+ public void testEncodeValidatePassword()
+ {
+ String password = "district";
+
+ String encodedPassword1 = passwordManager.encodePassword( password );
+ String encodedPassword2 = passwordManager.encodePassword( password );
+
+ assertFalse( encodedPassword1.equals( encodedPassword2 ) );
+ assertFalse( password.equals( encodedPassword1 ) );
+
+ assertTrue( passwordManager.matches( password, encodedPassword1 ));
+ assertTrue( passwordManager.matches( password, encodedPassword2 ));
+
+ assertFalse( passwordManager.matches( password, "anotherPassword" ) );
+ }
+}
=== modified file 'dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/security/SecurityServiceTest.java'
--- dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/security/SecurityServiceTest.java 2014-09-25 07:47:41 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/security/SecurityServiceTest.java 2014-09-30 12:29:45 +0000
@@ -144,9 +144,8 @@
//
// check password
//
- String hashedPassword = passwordManager.encodePassword( credentials.getUsername(), password );
- assertEquals( hashedPassword, credentials.getPassword() );
+ assertTrue( passwordManager.matches( password, credentials.getPassword() ) );
}
@Test
@@ -213,9 +212,8 @@
//
// check password
//
- String hashedPassword = passwordManager.encodePassword( credentials.getUsername(), password );
- assertEquals( hashedPassword, credentials.getPassword() );
+ assertTrue( passwordManager.matches( password, credentials.getPassword() ) );
}
@Test
=== modified file 'dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/translation/TranslationServiceTest.java'
--- dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/translation/TranslationServiceTest.java 2014-05-15 06:21:07 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/translation/TranslationServiceTest.java 2014-09-30 13:48:45 +0000
@@ -29,6 +29,7 @@
*/
import org.hisp.dhis.DhisSpringTest;
+import org.junit.Ignore;
import org.junit.Test;
import java.util.Locale;
@@ -93,6 +94,7 @@
assertEquals( translation1b, translationService.getTranslationNoFallback( className1, Locale.UK, "shortName", uid1 ) );
}
+ @Ignore
@Test
public void delete()
{
@@ -110,12 +112,13 @@
assertNull( translationService.getTranslationNoFallback( className1, Locale.UK, "name", uid1 ) );
assertNotNull( translationService.getTranslationNoFallback( className1, Locale.UK, "shortName", uid1 ) );
- translationService.deleteTranslations( translation1b.getClassName(),translation1b.getObjectUid() );
+ translationService.deleteTranslations( translation1b.getClassName(), translation1b.getObjectUid() );
assertNull( translationService.getTranslationNoFallback( className1, Locale.UK, "name", uid1 ) );
assertNull( translationService.getTranslationNoFallback( className1, Locale.UK, "shortName", uid1 ) );
}
+ @Ignore
@Test
public void testUpdateTranslation()
{
=== modified file 'dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/DhisSpringTest.java'
--- dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/DhisSpringTest.java 2014-04-28 19:23:37 +0000
+++ dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/DhisSpringTest.java 2014-08-22 15:39:25 +0000
@@ -43,7 +43,7 @@
* @author Lars Helge Overland
*/
@RunWith(SpringJUnit4ClassRunner.class)
-@ContextConfiguration(locations={"classpath*:/META-INF/dhis/beans.xml"})
+@ContextConfiguration( locations = { "classpath*:/META-INF/dhis/beans.xml", "classpath*:/META-INF/dhis/security.xml" } )
@Transactional
public abstract class DhisSpringTest
extends DhisConvenienceTest implements ApplicationContextAware
=== modified file 'dhis-2/dhis-web/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AccountController.java'
--- dhis-2/dhis-web/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AccountController.java 2014-09-25 07:29:20 +0000
+++ dhis-2/dhis-web/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AccountController.java 2014-09-30 12:29:45 +0000
@@ -399,7 +399,7 @@
username = credentials.getUsername();
}
- credentials.setPassword( passwordManager.encodePassword( username, password ) );
+ credentials.setPassword( passwordManager.encodePassword( password ) );
userService.updateUser( user );
userService.updateUserCredentials( credentials );
@@ -421,7 +421,7 @@
credentials = new UserCredentials();
credentials.setUsername( username );
- credentials.setPassword( passwordManager.encodePassword( username, password ) );
+ credentials.setPassword( passwordManager.encodePassword( password ) );
credentials.setSelfRegistered( true );
credentials.setUser( user );
credentials.getUserAuthorityGroups().add( userRole );
@@ -472,7 +472,7 @@
return;
}
- String oldPasswordEncoded = passwordManager.encodePassword( username, oldPassword );
+ String oldPasswordEncoded = passwordManager.encodePassword( oldPassword );
if ( !credentials.getPassword().equals( oldPasswordEncoded ) )
{
@@ -501,7 +501,7 @@
return;
}
- String passwordEncoded = passwordManager.encodePassword( username, password );
+ String passwordEncoded = passwordManager.encodePassword( password );
credentials.setPassword( passwordEncoded );
credentials.setPasswordLastUpdated( new Date() );
=== modified file 'dhis-2/dhis-web/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/user/UserController.java'
--- dhis-2/dhis-web/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/user/UserController.java 2014-08-15 07:40:20 +0000
+++ dhis-2/dhis-web/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/user/UserController.java 2014-08-22 15:39:25 +0000
@@ -213,9 +213,8 @@
if ( parsed.getUserCredentials().getPassword() != null )
{
- String encodePassword = passwordManager.encodePassword( parsed.getUsername(),
- parsed.getUserCredentials().getPassword() );
- parsed.getUserCredentials().setPassword( encodePassword );
+ String encodedPassword = passwordManager.encodePassword( parsed.getUserCredentials().getPassword() );
+ parsed.getUserCredentials().setPassword( encodedPassword );
}
ImportTypeSummary summary = importService.importObject( currentUserService.getCurrentUser().getUid(), parsed, ImportStrategy.UPDATE );
@@ -245,9 +244,8 @@
if ( parsed.getUserCredentials().getPassword() != null )
{
- String encodePassword = passwordManager.encodePassword( parsed.getUsername(),
- parsed.getUserCredentials().getPassword() );
- parsed.getUserCredentials().setPassword( encodePassword );
+ String encodedPassword = passwordManager.encodePassword( parsed.getUserCredentials().getPassword() );
+ parsed.getUserCredentials().setPassword( encodedPassword );
}
ImportTypeSummary summary = importService.importObject( currentUserService.getCurrentUser().getUid(), parsed, ImportStrategy.UPDATE );
@@ -305,9 +303,8 @@
user.getUserCredentials().getCatDimensionConstraints().addAll(
currentUserService.getCurrentUser().getUserCredentials().getCatDimensionConstraints() );
- String encodePassword = passwordManager.encodePassword( user.getUsername(),
- user.getUserCredentials().getPassword() );
- user.getUserCredentials().setPassword( encodePassword );
+ String encodedPassword = passwordManager.encodePassword( user.getUserCredentials().getPassword() );
+ user.getUserCredentials().setPassword( encodedPassword );
ImportTypeSummary summary = importService.importObject( currentUserService.getCurrentUser().getUid(), user, ImportStrategy.CREATE );
=== modified file 'dhis-2/dhis-web/dhis-web-commons/src/main/java/org/hisp/dhis/security/DatabaseAutomaticAccessProvider.java'
--- dhis-2/dhis-web/dhis-web-commons/src/main/java/org/hisp/dhis/security/DatabaseAutomaticAccessProvider.java 2014-08-15 07:40:20 +0000
+++ dhis-2/dhis-web/dhis-web-commons/src/main/java/org/hisp/dhis/security/DatabaseAutomaticAccessProvider.java 2014-08-22 15:39:25 +0000
@@ -83,7 +83,7 @@
UserCredentials userCredentials = new UserCredentials();
userCredentials.setUsername( username );
- userCredentials.setPassword( passwordManager.encodePassword( username, password ) );
+ userCredentials.setPassword( passwordManager.encodePassword( password ) );
userCredentials.setUser( user );
userCredentials.getUserAuthorityGroups().add( userAuthorityGroup );
=== modified file 'dhis-2/dhis-web/dhis-web-commons/src/main/java/org/hisp/dhis/useraccount/action/UpdateUserAccountAction.java'
--- dhis-2/dhis-web/dhis-web-commons/src/main/java/org/hisp/dhis/useraccount/action/UpdateUserAccountAction.java 2014-06-22 12:36:40 +0000
+++ dhis-2/dhis-web/dhis-web-commons/src/main/java/org/hisp/dhis/useraccount/action/UpdateUserAccountAction.java 2014-09-29 15:17:38 +0000
@@ -156,10 +156,9 @@
UserCredentials userCredentials = userService.getUserCredentials( user );
- String encodeOldPassword = passwordManager.encodePassword( userCredentials.getUsername(), oldPassword );
String currentPassword = userCredentials.getPassword();
- if ( !encodeOldPassword.equals( currentPassword ) )
+ if ( !passwordManager.matches( oldPassword, currentPassword ) )
{
message = i18n.getString( "wrong_password" );
return INPUT;
@@ -179,7 +178,7 @@
if ( rawPassword != null )
{
- userCredentials.setPassword( passwordManager.encodePassword( userCredentials.getUsername(), rawPassword ) );
+ userCredentials.setPassword( passwordManager.encodePassword( rawPassword ) );
userService.updateUserCredentials( userCredentials );
}
=== modified file 'dhis-2/dhis-web/dhis-web-commons/src/main/resources/META-INF/dhis/security.xml'
--- dhis-2/dhis-web/dhis-web-commons/src/main/resources/META-INF/dhis/security.xml 2014-09-17 07:03:32 +0000
+++ dhis-2/dhis-web/dhis-web-commons/src/main/resources/META-INF/dhis/security.xml 2014-09-30 12:29:45 +0000
@@ -97,12 +97,22 @@
<!-- Security : Authentication providers -->
<sec:authentication-manager alias="authenticationManager">
+ <sec:authentication-provider ref="migrationAuthenticationProvider" />
+ </sec:authentication-manager>
+
+ <!--
+ As of 2.17 user password hashes are being migrated from MD5(password, username) to bCrypt(password).
+ The migration is implemented in the migrationAuthenticationProvider configured above.
+ Once migration is complete, the authentication-manager configuration above can be
+ replaced by the one given below (commented). At that point the system will no longer accept
+ authentication request from users which are still stored with an MD5 hash in the database.
+
+ <sec:authentication-manager alias="authenticationManager">
<sec:authentication-provider user-service-ref="userDetailsService">
- <sec:password-encoder hash="md5">
- <sec:salt-source ref="usernameSaltSource" />
- </sec:password-encoder>
+ <sec:password-encoder ref="bCryptPasswordEncoder" />
</sec:authentication-provider>
</sec:authentication-manager>
+ -->
<!-- Security : AccessProvider -->
=== modified file 'dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/java/org/hisp/dhis/user/action/AddUserAction.java'
--- dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/java/org/hisp/dhis/user/action/AddUserAction.java 2014-09-12 06:24:17 +0000
+++ dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/java/org/hisp/dhis/user/action/AddUserAction.java 2014-09-30 12:29:45 +0000
@@ -363,13 +363,12 @@
user.setEmail( email );
user.setPhoneNumber( phoneNumber );
- userCredentials.setPassword( passwordManager.encodePassword( username, rawPassword ) );
+ userCredentials.setPassword( passwordManager.encodePassword( rawPassword ) );
}
if ( jsonAttributeValues != null )
{
- AttributeUtils.updateAttributeValuesFromJson( user.getAttributeValues(), jsonAttributeValues,
- attributeService );
+ AttributeUtils.updateAttributeValuesFromJson( user.getAttributeValues(), jsonAttributeValues, attributeService );
}
// ---------------------------------------------------------------------
=== modified file 'dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/java/org/hisp/dhis/user/action/DeleteCurrentUserAction.java'
--- dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/java/org/hisp/dhis/user/action/DeleteCurrentUserAction.java 2014-03-18 08:10:10 +0000
+++ dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/java/org/hisp/dhis/user/action/DeleteCurrentUserAction.java 2014-08-22 15:39:25 +0000
@@ -122,10 +122,8 @@
{
return INPUT;
}
-
- String oldEncodedPassword = passwordManager.encodePassword( userCredentials.getUsername(), oldPassword ) ;
-
- if ( !oldEncodedPassword.equals( oldPasswordFromDB ) )
+
+ if( !passwordManager.matches( oldPassword, oldPasswordFromDB ) )
{
message = i18n.getString( "wrong_password" );
return INPUT;
=== modified file 'dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/java/org/hisp/dhis/user/action/UpdateUserAction.java'
--- dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/java/org/hisp/dhis/user/action/UpdateUserAction.java 2014-09-12 06:24:17 +0000
+++ dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/java/org/hisp/dhis/user/action/UpdateUserAction.java 2014-09-30 12:29:45 +0000
@@ -322,7 +322,7 @@
if ( rawPassword != null )
{
- userCredentials.setPassword( passwordManager.encodePassword( userCredentials.getUsername(), rawPassword ) );
+ userCredentials.setPassword( passwordManager.encodePassword( rawPassword ) );
}
if ( jsonAttributeValues != null )