[KEYCLOAK-405] - Feature that doesn't allow old password to be reused

This commit is contained in:
girirajsharma
2015-04-13 20:59:43 +05:30
parent 375d061f58
commit e3bb61248a
19 changed files with 551 additions and 97 deletions

View File

@@ -1,11 +1,15 @@
package org.keycloak.models;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@@ -18,6 +22,7 @@ public class PasswordPolicy {
public static final String INVALID_PASSWORD_MIN_SPECIAL_CHARS_MESSAGE = "invalidPasswordMinSpecialCharsMessage";
public static final String INVALID_PASSWORD_NOT_USERNAME = "invalidPasswordNotUsernameMessage";
public static final String INVALID_PASSWORD_REGEX_PATTERN = "invalidPasswordRegexPatternMessage";
public static final String INVALID_PASSWORD_HISTORY = "invalidPasswordHistoryMessage";
private List<Policy> policies;
private String policyString;
@@ -67,10 +72,12 @@ public class PasswordPolicy {
} else if (name.equals(HashIterations.NAME)) {
list.add(new HashIterations(args));
} else if (name.equals(RegexPatterns.NAME)) {
for(String regexPattern : args) {
for (String regexPattern : args) {
Pattern.compile(regexPattern);
}
list.add(new RegexPatterns(args));
} else if (name.equals(PasswordHistory.NAME)) {
list.add(new PasswordHistory(args));
}
}
return list;
@@ -92,9 +99,35 @@ public class PasswordPolicy {
return -1;
}
public Error validate(String username, String password) {
/**
*
* @return -1 if no expired passwords setting
*/
public int getExpiredPasswords() {
if (policies == null)
return -1;
for (Policy p : policies) {
Error error = p.validate(username, password);
if (p instanceof PasswordHistory) {
return ((PasswordHistory) p).passwordHistoryPolicyValue;
}
}
return -1;
}
public Error validate(UserModel user, String password) {
for (Policy p : policies) {
Error error = p.validate(user, password);
if (error != null) {
return error;
}
}
return null;
}
public Error validate(String user, String password) {
for (Policy p : policies) {
Error error = p.validate(user, password);
if (error != null) {
return error;
}
@@ -103,7 +136,8 @@ public class PasswordPolicy {
}
private static interface Policy {
public Error validate(String username, String password);
public Error validate(UserModel user, String password);
public Error validate(String user, String password);
}
public static class Error {
@@ -131,9 +165,15 @@ public class PasswordPolicy {
public HashIterations(String[] args) {
iterations = intArg(NAME, 1, args);
}
@Override
public Error validate(String username, String password) {
public Error validate(String user, String password) {
return null;
}
@Override
public Error validate(UserModel user, String password) {
return null;
}
}
@@ -148,6 +188,11 @@ public class PasswordPolicy {
public Error validate(String username, String password) {
return username.equals(password) ? new Error(INVALID_PASSWORD_NOT_USERNAME) : null;
}
@Override
public Error validate(UserModel user, String password) {
return validate(user.getUsername(), password);
}
}
private static class Length implements Policy {
@@ -157,11 +202,17 @@ public class PasswordPolicy {
public Length(String[] args) {
min = intArg(NAME, 8, args);
}
@Override
public Error validate(String username, String password) {
return password.length() < min ? new Error(INVALID_PASSWORD_MIN_LENGTH_MESSAGE, min) : null;
}
@Override
public Error validate(UserModel user, String password) {
return validate(user.getUsername(), password);
}
}
private static class Digits implements Policy {
@@ -171,6 +222,7 @@ public class PasswordPolicy {
public Digits(String[] args) {
min = intArg(NAME, 1, args);
}
@Override
public Error validate(String username, String password) {
@@ -182,6 +234,11 @@ public class PasswordPolicy {
}
return count < min ? new Error(INVALID_PASSWORD_MIN_DIGITS_MESSAGE, min) : null;
}
@Override
public Error validate(UserModel user, String password) {
return validate(user.getUsername(), password);
}
}
private static class LowerCase implements Policy {
@@ -191,7 +248,7 @@ public class PasswordPolicy {
public LowerCase(String[] args) {
min = intArg(NAME, 1, args);
}
@Override
public Error validate(String username, String password) {
int count = 0;
@@ -202,6 +259,11 @@ public class PasswordPolicy {
}
return count < min ? new Error(INVALID_PASSWORD_MIN_LOWER_CASE_CHARS_MESSAGE, min) : null;
}
@Override
public Error validate(UserModel user, String password) {
return validate(user.getUsername(), password);
}
}
private static class UpperCase implements Policy {
@@ -222,6 +284,11 @@ public class PasswordPolicy {
}
return count < min ? new Error(INVALID_PASSWORD_MIN_UPPER_CASE_CHARS_MESSAGE, min) : null;
}
@Override
public Error validate(UserModel user, String password) {
return validate(user.getUsername(), password);
}
}
private static class SpecialChars implements Policy {
@@ -231,7 +298,7 @@ public class PasswordPolicy {
public SpecialChars(String[] args) {
min = intArg(NAME, 1, args);
}
@Override
public Error validate(String username, String password) {
int count = 0;
@@ -242,6 +309,11 @@ public class PasswordPolicy {
}
return count < min ? new Error(INVALID_PASSWORD_MIN_SPECIAL_CHARS_MESSAGE, min) : null;
}
@Override
public Error validate(UserModel user, String password) {
return validate(user.getUsername(), password);
}
}
private static class RegexPatterns implements Policy {
@@ -256,17 +328,96 @@ public class PasswordPolicy {
public Error validate(String username, String password) {
Pattern pattern = null;
Matcher matcher = null;
for(String regexPattern : regexPatterns) {
for (String regexPattern : regexPatterns) {
pattern = Pattern.compile(regexPattern);
matcher = pattern.matcher(password);
if (!matcher.matches()) {
return new Error(INVALID_PASSWORD_REGEX_PATTERN, (Object)regexPatterns);
return new Error(INVALID_PASSWORD_REGEX_PATTERN, (Object) regexPatterns);
}
}
return null;
}
@Override
public Error validate(UserModel user, String password) {
return validate(user.getUsername(), password);
}
}
private static class PasswordHistory implements Policy {
private static final String NAME = "passwordHistory";
private int passwordHistoryPolicyValue;
public PasswordHistory(String[] args) {
passwordHistoryPolicyValue = intArg(NAME, 3, args);
}
@Override
public Error validate(String user, String password) {
return null;
}
@Override
public Error validate(UserModel user, String password) {
if (passwordHistoryPolicyValue != -1) {
UserCredentialValueModel cred = getCredentialValueModel(user, UserCredentialModel.PASSWORD);
if (cred != null) {
if(new Pbkdf2PasswordEncoder(cred.getSalt()).verify(password, cred.getValue(), cred.getHashIterations())) {
return new Error(INVALID_PASSWORD_HISTORY, password);
}
}
List<UserCredentialValueModel> passwordExpiredCredentials = getCredentialValueModels(user, passwordHistoryPolicyValue - 1,
UserCredentialModel.PASSWORD_HISTORY);
for (UserCredentialValueModel credential : passwordExpiredCredentials) {
if (new Pbkdf2PasswordEncoder(credential.getSalt()).verify(password, credential.getValue(), credential.getHashIterations())) {
return new Error(INVALID_PASSWORD_HISTORY, password);
}
}
}
return null;
}
private UserCredentialValueModel getCredentialValueModel(UserModel user, String credType) {
for (UserCredentialValueModel model : user.getCredentialsDirectly()) {
if (model.getType().equals(credType)) {
return model;
}
}
return null;
}
private List<UserCredentialValueModel> getCredentialValueModels(UserModel user, int expiredPasswordsPolicyValue,
String credType) {
List<UserCredentialValueModel> credentialModels = new ArrayList<UserCredentialValueModel>();
for (UserCredentialValueModel model : user.getCredentialsDirectly()) {
if (model.getType().equals(credType)) {
credentialModels.add(model);
}
}
Collections.sort(credentialModels, new Comparator<UserCredentialValueModel>() {
public int compare(UserCredentialValueModel credFirst, UserCredentialValueModel credSecond) {
if (credFirst.getCreatedDate() > credSecond.getCreatedDate()) {
return -1;
} else if (credFirst.getCreatedDate() < credSecond.getCreatedDate()) {
return 1;
} else {
return 0;
}
}
});
if (credentialModels.size() > expiredPasswordsPolicyValue) {
return credentialModels.subList(0, expiredPasswordsPolicyValue);
}
return credentialModels;
}
}
private static int intArg(String policy, int defaultValue, String... args) {
if (args == null || args.length == 0) {
return defaultValue;

View File

@@ -8,6 +8,7 @@ import java.util.UUID;
*/
public class UserCredentialModel {
public static final String PASSWORD = "password";
public static final String PASSWORD_HISTORY = "password-history";
public static final String PASSWORD_TOKEN = "password-token";
// Secret is same as password but it is not hashed

View File

@@ -12,6 +12,7 @@ public class UserCredentialValueModel {
private String device;
private byte[] salt;
private int hashIterations;
private long createdDate;
public String getType() {
return type;
@@ -52,4 +53,13 @@ public class UserCredentialValueModel {
public void setHashIterations(int iterations) {
this.hashIterations = iterations;
}
public long getCreatedDate() {
return createdDate;
}
public void setCreatedDate(long createdDate) {
this.createdDate = createdDate;
}
}

View File

@@ -323,7 +323,7 @@ public class UserFederationManager implements UserProvider {
public void updateCredential(RealmModel realm, UserModel user, UserCredentialModel credential) {
if (credential.getType().equals(UserCredentialModel.PASSWORD)) {
if (realm.getPasswordPolicy() != null) {
PasswordPolicy.Error error = realm.getPasswordPolicy().validate(user.getUsername(), credential.getValue());
PasswordPolicy.Error error = realm.getPasswordPolicy().validate(user, credential.getValue());
if (error != null) throw new ModelException(error.getMessage(), error.getParameters());
}
}

View File

@@ -5,11 +5,23 @@ package org.keycloak.models.entities;
*/
public class CredentialEntity {
private String id;
private String type;
private String value;
private String device;
private byte[] salt;
private int hashIterations;
private long createdDate;
private UserEntity user;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getType() {
return type;
@@ -50,4 +62,21 @@ public class CredentialEntity {
public void setHashIterations(int hashIterations) {
this.hashIterations = hashIterations;
}
public long getCreatedDate() {
return createdDate;
}
public void setCreatedDate(long createdDate) {
this.createdDate = createdDate;
}
public UserEntity getUser() {
return user;
}
public void setUser(UserEntity user) {
this.user = user;
}
}