Index: lams_admin/conf/language/lams/ApplicationResources.properties =================================================================== diff -u -r8ebe651ed3972b4e53c75dd45e9f0ba1f9b63b3d -rd4e455d6806b6c48cca0b6e8ee87f256a92f123a --- lams_admin/conf/language/lams/ApplicationResources.properties (.../ApplicationResources.properties) (revision 8ebe651ed3972b4e53c75dd45e9f0ba1f9b63b3d) +++ lams_admin/conf/language/lams/ApplicationResources.properties (.../ApplicationResources.properties) (revision d4e455d6806b6c48cca0b6e8ee87f256a92f123a) @@ -460,6 +460,7 @@ config.password.numerics = Must contain at least a number config.password.symbols = Must contain at least one symbol(s) config.password.expiration = Password expiration in months (0 = never) +config.password.history = Do not allow this many last passwords (0 = off) error.password.mismatch = Your passwords do not match. error.password.empty = Password cannot be empty. admin.user.change.password = Force password change Index: lams_central/conf/language/lams/ApplicationResources.properties =================================================================== diff -u -r8ebe651ed3972b4e53c75dd45e9f0ba1f9b63b3d -rd4e455d6806b6c48cca0b6e8ee87f256a92f123a --- lams_central/conf/language/lams/ApplicationResources.properties (.../ApplicationResources.properties) (revision 8ebe651ed3972b4e53c75dd45e9f0ba1f9b63b3d) +++ lams_central/conf/language/lams/ApplicationResources.properties (.../ApplicationResources.properties) (revision d4e455d6806b6c48cca0b6e8ee87f256a92f123a) @@ -7,6 +7,7 @@ error.general.3 = If the problem persists please contact your system administrator. error.newpassword.mismatch = Your new passwords do not match each other. error.oldpassword.mismatch = Your old password is not correct. +error.password.history = You have already used this password. heading.password.change.screen = Change password label.password.old.password = Old password label.password.new.password = New password @@ -405,14 +406,15 @@ label.mark.org.favorite = Mark course as favourite label.email.send.me.a.copy = Send me a copy label.password.max.length = must be less than 25 characters -label.password.min.length = must be at least {0} characters long +label.password.min.length = must be at the least {0} characters long label.password.old.must.entered = Old password must be entered label.password.symbols.allowed = Only these symbols are allowed label.password.restrictions = Password must follow the restrictions shown above label.password.must.contain = Password must contain label.password.must.ucase = at least 1 upper case letter label.password.must.number = at least 1 number label.password.must.symbol = at least 1 symbol +label.password.history = must not be the same as last {0} passwords label.create.lesson = Create new lesson label.organisations = Select course with the lessons that needs to be export label.lesson.id = Lesson ID Index: lams_central/src/java/org/lamsfoundation/lams/web/PasswordChangeController.java =================================================================== diff -u -r4c272c96c3885f945357ffff697c662ff04d2e75 -rd4e455d6806b6c48cca0b6e8ee87f256a92f123a --- lams_central/src/java/org/lamsfoundation/lams/web/PasswordChangeController.java (.../PasswordChangeController.java) (revision 4c272c96c3885f945357ffff697c662ff04d2e75) +++ lams_central/src/java/org/lamsfoundation/lams/web/PasswordChangeController.java (.../PasswordChangeController.java) (revision d4e455d6806b6c48cca0b6e8ee87f256a92f123a) @@ -107,6 +107,10 @@ errorMap.add("password", messageService.getMessage("label.password.restrictions")); PasswordChangeController.log.debug("Password must follow the restrictions"); } + if (!ValidationUtil.isPasswordNotInHistory(password, user.getPasswordHistory().values())) { + errorMap.add("password", messageService.getMessage("error.password.history")); + PasswordChangeController.log.debug("Password has been recently used"); + } if (errorMap.isEmpty()) { userManagementService.updatePassword(user, password); @@ -122,7 +126,8 @@ } catch (Exception e) { PasswordChangeController.log.error("Exception occured ", e); - errorMap.add("GLOBAL", messageService.getMessage(e.getMessage())); + errorMap.add("GLOBAL", + "Error while chaging password" + (e.getMessage() == null ? "" : ": " + e.getMessage())); } // -- Report any errors Index: lams_central/web/passwordChangeContent.jsp =================================================================== diff -u -r8ebe651ed3972b4e53c75dd45e9f0ba1f9b63b3d -rd4e455d6806b6c48cca0b6e8ee87f256a92f123a --- lams_central/web/passwordChangeContent.jsp (.../passwordChangeContent.jsp) (revision 8ebe651ed3972b4e53c75dd45e9f0ba1f9b63b3d) +++ lams_central/web/passwordChangeContent.jsp (.../passwordChangeContent.jsp) (revision d4e455d6806b6c48cca0b6e8ee87f256a92f123a) @@ -9,6 +9,7 @@ <%=Configuration.get(ConfigurationKeys.PASSWORD_POLICY_LOWERCASE)%> <%=Configuration.get(ConfigurationKeys.PASSWORD_POLICY_NUMERICS)%> <%=Configuration.get(ConfigurationKeys.PASSWORD_POLICY_SYMBOLS)%> +<%=Configuration.get(ConfigurationKeys.PASSWORD_HISTORY_LIMIT)%> @@ -136,7 +137,14 @@
  • -
    + + +
  • + + + +
  • +
    Index: lams_common/src/java/org/lamsfoundation/lams/dbupdates/patch20210215.sql =================================================================== diff -u -r8ebe651ed3972b4e53c75dd45e9f0ba1f9b63b3d -rd4e455d6806b6c48cca0b6e8ee87f256a92f123a --- lams_common/src/java/org/lamsfoundation/lams/dbupdates/patch20210215.sql (.../patch20210215.sql) (revision 8ebe651ed3972b4e53c75dd45e9f0ba1f9b63b3d) +++ lams_common/src/java/org/lamsfoundation/lams/dbupdates/patch20210215.sql (.../patch20210215.sql) (revision d4e455d6806b6c48cca0b6e8ee87f256a92f123a) @@ -6,11 +6,23 @@ --LDEV-5178 Add password expiration INSERT INTO lams_configuration (config_key, config_value, description_key, header_name, format, required) -VALUES ('PasswordExpirationMonths','12', 'config.password.expiration', 'config.header.password.policy', 'LONG', 1); +VALUES ('PasswordExpirationMonths', '12', 'config.password.expiration', 'config.header.password.policy', 'LONG', 1), + ('PasswordHistoryLimit', '3', 'config.password.history', 'config.header.password.policy', 'LONG', 1); ALTER TABLE lams_user ADD COLUMN password_change_date DATETIME AFTER portrait_uuid; +CREATE TABLE lams_user_password_history ( + uid INT UNSIGNED AUTO_INCREMENT, + user_id BIGINT NOT NULL, + change_date DATETIME NOT NULL, + password CHAR(129) NOT NULL, + PRIMARY KEY (uid), + CONSTRAINT FK_lams_user_password_history_1 FOREIGN KEY (user_id) REFERENCES lams_user (user_id) + ON DELETE CASCADE ON UPDATE CASCADE +); + + -- Put all sql statements above here -- If there were no errors, commit and restore autocommit to on Index: lams_common/src/java/org/lamsfoundation/lams/usermanagement/User.java =================================================================== diff -u -r8ebe651ed3972b4e53c75dd45e9f0ba1f9b63b3d -rd4e455d6806b6c48cca0b6e8ee87f256a92f123a --- lams_common/src/java/org/lamsfoundation/lams/usermanagement/User.java (.../User.java) (revision 8ebe651ed3972b4e53c75dd45e9f0ba1f9b63b3d) +++ lams_common/src/java/org/lamsfoundation/lams/usermanagement/User.java (.../User.java) (revision d4e455d6806b6c48cca0b6e8ee87f256a92f123a) @@ -30,10 +30,13 @@ import java.util.Iterator; import java.util.LinkedHashSet; import java.util.Set; +import java.util.SortedMap; import java.util.TimeZone; +import java.util.TreeMap; import java.util.UUID; import javax.persistence.CascadeType; +import javax.persistence.CollectionTable; import javax.persistence.Column; import javax.persistence.ElementCollection; import javax.persistence.Entity; @@ -44,6 +47,7 @@ import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.ManyToOne; +import javax.persistence.MapKeyColumn; import javax.persistence.OneToMany; import javax.persistence.OneToOne; import javax.persistence.OrderBy; @@ -54,6 +58,7 @@ import org.apache.commons.lang.builder.ToStringBuilder; import org.hibernate.annotations.LazyCollection; import org.hibernate.annotations.LazyCollectionOption; +import org.hibernate.annotations.SortNatural; import org.lamsfoundation.lams.learningdesign.LearningDesign; import org.lamsfoundation.lams.lesson.LearnerProgress; import org.lamsfoundation.lams.lesson.Lesson; @@ -192,6 +197,16 @@ @Column(name = "change_password") private Boolean changePassword; + /** + * Contains a list of recently used passwords in form: password change date -> "old_hash=old_salt" + */ + @ElementCollection(fetch = FetchType.LAZY) + @CollectionTable(name = "lams_user_password_history", joinColumns = @JoinColumn(name = "user_id")) + @MapKeyColumn(name = "change_date") + @Column(name = "password") + @SortNatural + private SortedMap passwordHistory = new TreeMap<>(); + @Column(name = "first_login") private Boolean firstLogin; @@ -619,6 +634,14 @@ this.changePassword = changePassword; } + public SortedMap getPasswordHistory() { + return passwordHistory; + } + + public void setPasswordHistory(SortedMap passwordHistory) { + this.passwordHistory = passwordHistory; + } + public String getTimeZone() { if (timeZone == null) { return TimeZone.getDefault().getID(); Index: lams_common/src/java/org/lamsfoundation/lams/usermanagement/service/UserManagementService.java =================================================================== diff -u -r4c272c96c3885f945357ffff697c662ff04d2e75 -rd4e455d6806b6c48cca0b6e8ee87f256a92f123a --- lams_common/src/java/org/lamsfoundation/lams/usermanagement/service/UserManagementService.java (.../UserManagementService.java) (revision 4c272c96c3885f945357ffff697c662ff04d2e75) +++ lams_common/src/java/org/lamsfoundation/lams/usermanagement/service/UserManagementService.java (.../UserManagementService.java) (revision d4e455d6806b6c48cca0b6e8ee87f256a92f123a) @@ -40,6 +40,7 @@ import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.SortedMap; import java.util.UUID; import java.util.Vector; import java.util.stream.Collectors; @@ -102,6 +103,8 @@ private static final String SEQUENCES_FOLDER_NAME_KEY = "runsequences.folder.name"; + private static final int PASSWORD_HISTORY_DEFAULT_LIMIT = 50; + private IBaseDAO baseDAO; private IGroupDAO groupDAO; @@ -407,16 +410,27 @@ @Override public void updatePassword(User user, String password) { - try { - String salt = HashUtil.salt(); - user.setSalt(salt); - user.setPassword(HashUtil.sha256(password, salt)); - user.setModifiedDate(new Date()); - user.setPasswordChangeDate(LocalDateTime.now()); - baseDAO.update(user); - } catch (Exception e) { - log.debug(e); + String salt = HashUtil.salt(); + user.setSalt(salt); + String hash = HashUtil.sha256(password, salt); + user.setPassword(hash); + user.setModifiedDate(new Date()); + LocalDateTime date = LocalDateTime.now(); + user.setPasswordChangeDate(date); + + // add new password to history + SortedMap history = user.getPasswordHistory(); + history.put(date, hash + "=" + salt); + + // clear old password, about the limit + int historyLimit = Configuration.getAsInt(ConfigurationKeys.PASSWORD_HISTORY_LIMIT); + // if no limit is set then set some high limit to keep the table tidy + historyLimit = historyLimit <= 0 ? PASSWORD_HISTORY_DEFAULT_LIMIT : historyLimit; + while (historyLimit < history.size()) { + history.remove(history.firstKey()); } + + baseDAO.update(user); } @Override Index: lams_common/src/java/org/lamsfoundation/lams/util/ConfigurationKeys.java =================================================================== diff -u -r8ebe651ed3972b4e53c75dd45e9f0ba1f9b63b3d -rd4e455d6806b6c48cca0b6e8ee87f256a92f123a --- lams_common/src/java/org/lamsfoundation/lams/util/ConfigurationKeys.java (.../ConfigurationKeys.java) (revision 8ebe651ed3972b4e53c75dd45e9f0ba1f9b63b3d) +++ lams_common/src/java/org/lamsfoundation/lams/util/ConfigurationKeys.java (.../ConfigurationKeys.java) (revision d4e455d6806b6c48cca0b6e8ee87f256a92f123a) @@ -275,9 +275,11 @@ public static String PASSWORD_POLICY_NUMERICS = "PasswordPolicyNumerics"; public static String PASSWORD_POLICY_SYMBOLS = "PasswordPolicySymbols"; - + public static String PASSWORD_EXPIRATION_MONTHS = "PasswordExpirationMonths"; + public static String PASSWORD_HISTORY_LIMIT = "PasswordHistoryLimit"; + // LDEV-4049 Option for not displaying stacktraces in config settings public static String ERROR_STACK_TRACE = "ErrorStackTrace"; @@ -321,7 +323,7 @@ public static String WORKFLOW_AUTOMATION_REVIEW_REMINDER_PERIOD_DAYS = "WorkflowAutomationReviewPeriod"; public static String WORKFLOW_AUTOMATION_REVIEW_REMINDER_TIME = "WorkflowAutomationReviewTime"; public static String WORKFLOW_AUTOMATION_REVIEW_REMINDER_EMAILS = "WorkflowAutomationReviewEmails"; - + // LDEV-4997 Etherpad as service public static String ETHERPAD_SERVER_URL = "EtherpadServerUrl"; public static String ETHERPAD_API_KEY = "EtherpadApiKey"; Index: lams_common/src/java/org/lamsfoundation/lams/util/ValidationUtil.java =================================================================== diff -u -r7475d08afc280b5e2e5ddf04e8bf35e3166aaf80 -rd4e455d6806b6c48cca0b6e8ee87f256a92f123a --- lams_common/src/java/org/lamsfoundation/lams/util/ValidationUtil.java (.../ValidationUtil.java) (revision 7475d08afc280b5e2e5ddf04e8bf35e3166aaf80) +++ lams_common/src/java/org/lamsfoundation/lams/util/ValidationUtil.java (.../ValidationUtil.java) (revision d4e455d6806b6c48cca0b6e8ee87f256a92f123a) @@ -22,6 +22,7 @@ package org.lamsfoundation.lams.util; +import java.util.Collection; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -140,6 +141,26 @@ } /** + * Checks if password has not been used in recent history. + */ + public static boolean isPasswordNotInHistory(String newPassword, Collection hashesAndSalts) { + int historyLimit = Configuration.getAsInt(ConfigurationKeys.PASSWORD_HISTORY_LIMIT); + if (historyLimit <= 0) { + return true; + } + for (String hashAndSalt : hashesAndSalts) { + String[] hashAndSaltSplit = hashAndSalt.split("="); + String oldHash = hashAndSaltSplit[0]; + String oldSalt = hashAndSaltSplit[1]; + String newHash = HashUtil.sha256(newPassword, oldSalt); + if (oldHash.equals(newHash)) { + return false; + } + } + return true; + } + + /** * Checks whether supplied email address is valid. It validates email only if USER_VALIDATION_REQUIRED_EMAIL LAMS * configuration is ON. */ @@ -219,14 +240,15 @@ } int wordCount = 0; - if ( text.length() > 0) { + if (text.length() > 0) { String cleanedString = text.replaceAll("[\'\";:,\\.\\?\\-!]+", "").trim(); wordCount = cleanedString.split("\\S+").length;//.match(/\S+/g) || []) ; // special case - if only one word and no spaces then the split array is empty. - if ( wordCount == 0 && cleanedString.length() > 0) + if (wordCount == 0 && cleanedString.length() > 0) { wordCount = 1; + } } - + // check min words limit is reached return (wordCount >= minWordsLimit); }