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