mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-25 16:42:34 +00:00
[KEYCLOAK-405] - Feature that doesn't allow old password to be reused
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user