KEYCLOAK-10307: check password history length in password verification (#6058)

This commit is contained in:
pkokush 2019-10-24 22:33:21 +03:00 committed by Marek Posolda
parent 1905260eac
commit ff551c5545
4 changed files with 40 additions and 6 deletions

View file

@ -26,6 +26,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -60,7 +61,8 @@ public class HistoryPasswordPolicyProvider implements PasswordPolicyProvider {
} }
} }
List<CredentialModel> passwordHistory = session.userCredentialManager().getStoredCredentialsByType(realm, user, CredentialModel.PASSWORD_HISTORY); List<CredentialModel> passwordHistory = session.userCredentialManager().getStoredCredentialsByType(realm, user, CredentialModel.PASSWORD_HISTORY);
for (CredentialModel cred : passwordHistory) { List<CredentialModel> recentPasswordHistory = getRecent(passwordHistory, passwordHistoryPolicyValue - 1);
for (CredentialModel cred : recentPasswordHistory) {
PasswordHashProvider hash = session.getProvider(PasswordHashProvider.class, cred.getAlgorithm()); PasswordHashProvider hash = session.getProvider(PasswordHashProvider.class, cred.getAlgorithm());
if (hash.verify(password, cred)) { if (hash.verify(password, cred)) {
return new PolicyError(ERROR_MESSAGE, passwordHistoryPolicyValue); return new PolicyError(ERROR_MESSAGE, passwordHistoryPolicyValue);
@ -71,6 +73,13 @@ public class HistoryPasswordPolicyProvider implements PasswordPolicyProvider {
return null; return null;
} }
private List<CredentialModel> getRecent(List<CredentialModel> passwordHistory, int limit) {
return passwordHistory.stream()
.sorted(CredentialModel.comparingByStartDateDesc())
.limit(limit)
.collect(Collectors.toList());
}
@Override @Override
public Object parseConfig(String value) { public Object parseConfig(String value) {
return parseInteger(value, HistoryPasswordPolicyProviderFactory.DEFAULT_VALUE); return parseInteger(value, HistoryPasswordPolicyProviderFactory.DEFAULT_VALUE);

View file

@ -20,6 +20,7 @@ package org.keycloak.credential;
import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.common.util.MultivaluedHashMap;
import java.io.Serializable; import java.io.Serializable;
import java.util.Comparator;
/** /**
* Used just in cases when we want to "directly" update or retrieve the hash or salt of user credential (For example during export/import) * Used just in cases when we want to "directly" update or retrieve the hash or salt of user credential (For example during export/import)
@ -168,4 +169,12 @@ public class CredentialModel implements Serializable {
public void setConfig(MultivaluedHashMap<String, String> config) { public void setConfig(MultivaluedHashMap<String, String> config) {
this.config = config; this.config = config;
} }
public static Comparator<CredentialModel> comparingByStartDateDesc() {
return (o1, o2) -> { // sort by date descending
Long o1Date = o1.getCreatedDate() == null ? Long.MIN_VALUE : o1.getCreatedDate();
Long o2Date = o2.getCreatedDate() == null ? Long.MIN_VALUE : o2.getCreatedDate();
return (-o1Date.compareTo(o2Date));
};
}
} }

View file

@ -118,11 +118,7 @@ public class PasswordCredentialProvider implements CredentialProvider, Credentia
final int passwordsToLeave = expiredPasswordsPolicyValue - 2; final int passwordsToLeave = expiredPasswordsPolicyValue - 2;
if (list.size() > passwordsToLeave) { if (list.size() > passwordsToLeave) {
list.stream() list.stream()
.sorted((o1, o2) -> { // sort by date descending .sorted(CredentialModel.comparingByStartDateDesc())
Long o1Date = o1.getCreatedDate() == null ? Long.MIN_VALUE : o1.getCreatedDate();
Long o2Date = o2.getCreatedDate() == null ? Long.MIN_VALUE : o2.getCreatedDate();
return (- o1Date.compareTo(o2Date));
})
.skip(passwordsToLeave) .skip(passwordsToLeave)
.forEach(p -> getCredentialStore().removeStoredCredential(realm, user, p.getId())); .forEach(p -> getCredentialStore().removeStoredCredential(realm, user, p.getId()));
} }

View file

@ -561,6 +561,26 @@ public class AccountFormServiceTest extends AbstractTestRealmKeycloakTest {
assertNumberOfStoredCredentials(1); assertNumberOfStoredCredentials(1);
} }
@Test
public void changePasswordToOldOneAfterPasswordHistoryPolicyExpirationChange() {
userId = createUser("test", "user-changePasswordToOldOneAfterPasswordHistoryPolicyExpirationChange", "password");
setPasswordPolicy(PasswordPolicy.PASSWORD_HISTORY_ID + "(3)");
changePasswordPage.open();
loginPage.login("user-changePasswordToOldOneAfterPasswordHistoryPolicyExpirationChange", "password");
events.expectLogin().user(userId).client("account").detail(Details.REDIRECT_URI, getAccountRedirectUrl() + "?path=password").assertEvent();
assertNumberOfStoredCredentials(1);
assertChangePasswordSucceeds("password", "password1");
assertNumberOfStoredCredentials(2);
assertChangePasswordSucceeds("password1", "password2");
assertNumberOfStoredCredentials(3);
setPasswordPolicy(PasswordPolicy.PASSWORD_HISTORY_ID + "(2)");
assertChangePasswordSucceeds("password2", "password");
}
@Test @Test
public void changePasswordWithPasswordHistoryPolicyExpiration() { public void changePasswordWithPasswordHistoryPolicyExpiration() {
userId = createUser("test", "user-changePasswordWithPasswordHistoryPolicyExpiration", "password"); userId = createUser("test", "user-changePasswordWithPasswordHistoryPolicyExpiration", "password");