Merge pull request #1132 from girirajsharma/master

[KEYCLOAK-405] - Feature that doesn't allow old password to be reused
This commit is contained in:
Stian Thorgersen 2015-04-15 06:53:07 +02:00
commit 47da227809
22 changed files with 761 additions and 105 deletions

View file

@ -32,6 +32,9 @@
<constraints nullable="false"/>
</column>
</createTable>
<addColumn tableName="CREDENTIAL">
<column name="CREATED_DATE" type="BIGINT"/>
</addColumn>
<addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_IDPM" tableName="IDENTITY_PROVIDER_MAPPER"/>
<addPrimaryKey columnNames="IDP_MAPPER_ID, NAME" constraintName="CONSTRAINT_IDPMConfig" tableName="IDP_MAPPER_CONFIG"/>
<addForeignKeyConstraint baseColumnNames="REALM_ID" baseTableName="IDENTITY_PROVIDER_MAPPER" constraintName="FK_IDPM_REALM" referencedColumnNames="ID" referencedTableName="REALM"/>

View file

@ -128,7 +128,8 @@
<para>
In the admin console, per realm, you can set up a password policy to enforce that users pick hard to guess passwords.
A password has to match all policies. The password policies that can be configured are hash iterations, length, digits,
lowercase, uppercase, special characters, not username and regex patterns. Multiple regex patterns, separated by comma,
lowercase, uppercase, special characters, not username, regex patterns and expired passwords. Expired Passwords policy
restricts a user from resetting his password to N old expired passwords. Multiple regex patterns, separated by comma,
can be specified. If there's more than one regex added, password has to match all fully.
Increasing number of Hash Iterations (n) does not worsen anything (and certainly not the cipher) and it greatly increases the
resistance to dictionary attacks. However the drawback to increasing n is that it has some cost (CPU usage, energy, delay) for

View file

@ -89,12 +89,14 @@ identityProviderRemovedMessage=Identity Provider erfolgreich entfernt.
accountDisabledMessage=Benutzerkonto ist gesperrt, bitte kontaktieren Sie den Admin.
accountTemporarilyDisabledMessage=Benutzerkonto ist tempor\u00E4r gesperrt, bitte kontaktieren Sie den Admin oder versuchen Sie es sp\u00E4ter noch einmal.
invalidPasswordMinLengthMessage=Ung\u00FCltiges Passwort: minimum l\u00E4nge {0}.
invalidPasswordMinDigitsMessage=Ung\u00FCltiges Passwort: muss mindestens {0} Zahl(en) beinhalten.
invalidPasswordMinLowerCaseCharsMessage=Ung\u00FCltiges Passwort: muss mindestens {0} Kleinbuchstaben beinhalten.
invalidPasswordMinUpperCaseCharsMessage=Ung\u00FCltiges Passwort: muss mindestens {0} Grossbuchstaben beinhalten.
invalidPasswordMinLengthMessage=Ung\u00FCltiges Passwort\: minimum l\u00E4nge {0}.
invalidPasswordMinDigitsMessage=Ung\u00FCltiges Passwort\: muss mindestens {0} Zahl(en) beinhalten.
invalidPasswordMinLowerCaseCharsMessage=Ung\u00FCltiges Passwort\: muss mindestens {0} Kleinbuchstaben beinhalten.
invalidPasswordMinUpperCaseCharsMessage=Ung\u00FCltiges Passwort\: muss mindestens {0} Grossbuchstaben beinhalten.
invalidPasswordMinSpecialCharsMessage=Ung\u00FCltiges Passwort\: muss mindestens {0} Spezialzeichen beinhalten.
invalidPasswordNotUsernameMessage=Ung\u00FCltiges Passwort\: darf nicht gleich sein wie Benutzername.
invalidPasswordRegexPatternMessage=Ung\u00FCltiges Passwort\: nicht Regex-Muster (n) entsprechen.
invalidPasswordHistoryMessage=Ung\u00FCltiges Passwort {0}: darf nicht gleich einem der letzten Passwortgeschichte.
locale_de=Deutsch
locale_en=Englisch

View file

@ -93,7 +93,9 @@ invalidPasswordMinLowerCaseCharsMessage=Invalid password: must contain at least
invalidPasswordMinDigitsMessage=Invalid password: must contain at least {0} numerical digits.
invalidPasswordMinUpperCaseCharsMessage=Invalid password: must contain at least {0} upper case characters.
invalidPasswordMinSpecialCharsMessage=Invalid password: must contain at least {0} special characters.
invalidPasswordNotUsernameMessage=Invalid password\: must not be equal to the username.
invalidPasswordNotUsernameMessage=Invalid password: must not be equal to the username.
invalidPasswordRegexPatternMessage=Invalid password: fails to match regex pattern(s).
invalidPasswordHistoryMessage=Invalid password {0}: must not be equal to any of last password history.
locale_de=German
locale_en=English

View file

@ -89,12 +89,14 @@ identityProviderRemovedMessage=Provedor de identidade removido com sucesso
accountDisabledMessage=Conta desativada, contate o administrador
accountTemporarilyDisabledMessage=A conta est\u00E1 temporariamente indispon\u00EDvel, contate administrador ou tente novamente mais tarde
invalidPasswordMinLengthMessage=Senha inv\u00E1lida: comprimento m\u00EDnimo {0}
invalidPasswordMinLowerCaseCharsMessage=Senha inv\u00E1lida: deve conter pelo menos {0} caracteres min\u00FAsculos
invalidPasswordMinDigitsMessage=Senha inv\u00E1lida: deve conter pelo menos {0} d\u00EDgitos num\u00E9ricos
invalidPasswordMinUpperCaseCharsMessage=Senha inv\u00E1lida: deve conter pelo menos {0} caracteres mai\u00FAsculos
invalidPasswordMinSpecialCharsMessage=Senha inv\u00E1lida: deve conter pelo menos {0} caracteres especiais
invalidPasswordMinLengthMessage=Senha inv\u00E1lida\: comprimento m\u00EDnimo {0}
invalidPasswordMinLowerCaseCharsMessage=Senha inv\u00E1lida\: deve conter pelo menos {0} caracteres min\u00FAsculos
invalidPasswordMinDigitsMessage=Senha inv\u00E1lida\: deve conter pelo menos {0} d\u00EDgitos num\u00E9ricos
invalidPasswordMinUpperCaseCharsMessage=Senha inv\u00E1lida\: deve conter pelo menos {0} caracteres mai\u00FAsculos
invalidPasswordMinSpecialCharsMessage=Senha inv\u00E1lida\: deve conter pelo menos {0} caracteres especiais
invalidPasswordNotUsernameMessage=Senha inv\u00E1lida\: n\u00E3o deve ser igual ao nome de usu\u00E1rio
invalidPasswordRegexPatternMessage=Senha inv\u00E1lida\: n\u00E3o correspondem ao padr\u00E3o regex(s).
invalidPasswordHistoryMessage=Senha inv\u00E1lida {0}\: n\u00E3o deve ser igual a qualquer uma \u00FAltima hist\u00F3ria senha.
locale_de=Deutsch
locale_en=English

View file

@ -907,7 +907,8 @@ module.factory('PasswordPolicy', function() {
upperCase: "Minimal number (integer type) of uppercase characters in password. Default value is 1.",
specialChars: "Minimal number (integer type) of special characters in password. Default value is 1.",
notUsername: "Block passwords that are equal to the username",
regexPatterns: "Block passwords that do not match all of the regex patterns (string type)."
regexPatterns: "Block passwords that do not match all of the regex patterns (string type).",
passwordHistory: "Block passwords that are equal to previous passwords. Default value is 3."
}
p.allPolicies = [
@ -918,7 +919,8 @@ module.factory('PasswordPolicy', function() {
{ name: 'upperCase', value: 1 },
{ name: 'specialChars', value: 1 },
{ name: 'notUsername', value: 1 },
{ name: 'regexPatterns', value: ''}
{ name: 'regexPatterns', value: ''},
{ name: 'passwordHistory', value: 3 }
];
p.parse = function(policyString) {

View file

@ -125,12 +125,14 @@ accountPasswordUpdatedMessage=Ihr Passwort wurde aktualisiert.
noAccessMessage=Kein Zugriff
invalidPasswordMinLengthMessage=Ung\u00FCltiges Passwort: minimum l\u00E4nge {0}.
invalidPasswordMinDigitsMessage=Ung\u00FCltiges Passwort: muss mindestens {0} Zahl(en) beinhalten.
invalidPasswordMinLowerCaseCharsMessage=Ung\u00FCltiges Passwort: muss mindestens {0} Kleinbuchstaben beinhalten.
invalidPasswordMinUpperCaseCharsMessage=Ung\u00FCltiges Passwort: muss mindestens {0} Grossbuchstaben beinhalten.
invalidPasswordMinSpecialCharsMessage=Ung\u00FCltiges Passwort: muss mindestens {0} Spezialzeichen beinhalten.
invalidPasswordMinLengthMessage=Ung\u00FCltiges Passwort\: minimum l\u00E4nge {0}.
invalidPasswordMinDigitsMessage=Ung\u00FCltiges Passwort\: muss mindestens {0} Zahl(en) beinhalten.
invalidPasswordMinLowerCaseCharsMessage=Ung\u00FCltiges Passwort\: muss mindestens {0} Kleinbuchstaben beinhalten.
invalidPasswordMinUpperCaseCharsMessage=Ung\u00FCltiges Passwort\: muss mindestens {0} Grossbuchstaben beinhalten.
invalidPasswordMinSpecialCharsMessage=Ung\u00FCltiges Passwort\: muss mindestens {0} Spezialzeichen beinhalten.
invalidPasswordNotUsernameMessage=Ung\u00FCltiges Passwort\: darf nicht gleich sein wie Benutzername.
invalidPasswordRegexPatternMessage=Ung\u00FCltiges Passwort\: nicht Regex-Muster (n) entsprechen.
invalidPasswordHistoryMessage=Ung\u00FCltiges Passwort {0}\: darf nicht gleich einem der letzten Passwortgeschichte.
failedToProcessResponseMessage=Konnte Response nicht verarbeiten.
httpsRequiredMessage=HTTPS erforderlich.

View file

@ -127,7 +127,9 @@ invalidPasswordMinDigitsMessage=Invalid password: must contain at least {0} nume
invalidPasswordMinLowerCaseCharsMessage=Invalid password: must contain at least {0} lower case characters.
invalidPasswordMinUpperCaseCharsMessage=Invalid password: must contain at least {0} upper case characters.
invalidPasswordMinSpecialCharsMessage=Invalid password: must contain at least {0} special characters.
invalidPasswordNotUsernameMessage=Invalid password\: must not be equal to the username.
invalidPasswordNotUsernameMessage=Invalid password: must not be equal to the username.
invalidPasswordRegexPatternMessage=Invalid password: fails to match regex pattern(s).
invalidPasswordHistoryMessage=Invalid password {0}: must not be equal to any of last password history.
failedToProcessResponseMessage=Failed to process response
httpsRequiredMessage=HTTPS required

View file

@ -122,12 +122,14 @@ accountPasswordUpdatedMessage=Sua senha foi atualizada
noAccessMessage=Sem acesso
invalidPasswordMinLengthMessage=Senha inv\u00E1lida: comprimento m\u00EDnimo {0}
invalidPasswordMinDigitsMessage=Senha inv\u00E1lida: deve conter pelo menos {0} d\u00EDgitos num\u00E9ricos
invalidPasswordMinLowerCaseCharsMessage=Senha inv\u00E1lida: deve conter pelo menos {0} caracteres min\u00FAsculos
invalidPasswordMinUpperCaseCharsMessage=Senha inv\u00E1lida: deve conter pelo menos {0} caracteres mai\u00FAsculos
invalidPasswordMinSpecialCharsMessage=Senha inv\u00E1lida: deve conter pelo menos {0} caracteres especiais
invalidPasswordNotUsernameMessage=Senha inv\u00E1lida: n\u00E3o deve ser igual ao nome de usu\u00E1rio
invalidPasswordMinLengthMessage=Senha inv\u00E1lida\: comprimento m\u00EDnimo {0}
invalidPasswordMinDigitsMessage=Senha inv\u00E1lida\: deve conter pelo menos {0} d\u00EDgitos num\u00E9ricos
invalidPasswordMinLowerCaseCharsMessage=Senha inv\u00E1lida\: deve conter pelo menos {0} caracteres min\u00FAsculos
invalidPasswordMinUpperCaseCharsMessage=Senha inv\u00E1lida\: deve conter pelo menos {0} caracteres mai\u00FAsculos
invalidPasswordMinSpecialCharsMessage=Senha inv\u00E1lida\: deve conter pelo menos {0} caracteres especiais
invalidPasswordNotUsernameMessage=Senha inv\u00E1lida\: n\u00E3o deve ser igual ao nome de usu\u00E1rio
invalidPasswordRegexPatternMessage=Senha inv\u00E1lida\: n\u00E3o correspondem ao padr\u00E3o regex(s).
invalidPasswordHistoryMessage=Senha inv\u00E1lida {0}\: n\u00E3o deve ser igual a qualquer uma \u00FAltima hist\u00F3ria senha.
failedToProcessResponseMessage=Falha ao processar a resposta
httpsRequiredMessage=HTTPS requerido

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 {
@ -132,8 +166,14 @@ public class PasswordPolicy {
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 {
@ -158,10 +203,16 @@ public class PasswordPolicy {
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 {
@ -172,6 +223,7 @@ public class PasswordPolicy {
min = intArg(NAME, 1, args);
}
@Override
public Error validate(String username, String password) {
int count = 0;
@ -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 {
@ -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 {
@ -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,15 +328,94 @@ 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) {

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

View file

@ -1,5 +1,9 @@
package org.keycloak.models;
import static org.junit.Assert.fail;
import java.util.regex.PatternSyntaxException;
import org.junit.Assert;
import org.junit.Test;
@ -80,6 +84,48 @@ public class PasswordPolicyTest {
Assert.assertNull(policy.validate("jdoe", "ab&d1234"));
}
@Test
public void testRegexPatterns() {
PasswordPolicy policy = null;
try {
policy = new PasswordPolicy("regexPatterns");
fail("Expected NullPointerEXception: Regex Pattern cannot be null.");
} catch (NullPointerException e) {
// Expected NPE as regex pattern is null.
}
try {
policy = new PasswordPolicy("regexPatterns(*)");
fail("Expected PatternSyntaxException: Regex Pattern cannot be null.");
} catch (PatternSyntaxException e) {
// Expected PSE as regex pattern(or any of its token) is not quantifiable.
}
try {
policy = new PasswordPolicy("regexPatterns(*,**)");
fail("Expected PatternSyntaxException: Regex Pattern cannot be null.");
} catch (PatternSyntaxException e) {
// Expected PSE as regex pattern(or any of its token) is not quantifiable.
}
//Fails to match one of the regex pattern
policy = new PasswordPolicy("regexPatterns(jdoe,j*d)");
Assert.assertEquals("invalidPasswordRegexPatternMessage", policy.validate("jdoe", "jdoe").getMessage());
////Fails to match all of the regex patterns
policy = new PasswordPolicy("regexPatterns(j*p,j*d,adoe)");
Assert.assertEquals("invalidPasswordRegexPatternMessage", policy.validate("jdoe", "jdoe").getMessage());
policy = new PasswordPolicy("regexPatterns([a-z][a-z][a-z][a-z][0-9])");
Assert.assertEquals("invalidPasswordRegexPatternMessage", policy.validate("jdoe", "jdoe").getMessage());
policy = new PasswordPolicy("regexPatterns(jdoe)");
Assert.assertNull(policy.validate("jdoe", "jdoe"));
policy = new PasswordPolicy("regexPatterns([a-z][a-z][a-z][a-z][0-9])");
Assert.assertNull(policy.validate("jdoe", "jdoe0"));
}
@Test
public void testComplex() {
PasswordPolicy policy = new PasswordPolicy("length(8) and digits(2) and lowerCase(2) and upperCase(2) and specialChars(2) and notUsername()");

View file

@ -17,6 +17,8 @@
package org.keycloak.models.file.adapter;
import org.keycloak.models.ClientModel;
import static org.keycloak.models.utils.Pbkdf2PasswordEncoder.getSalt;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
@ -28,11 +30,14 @@ import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.keycloak.connections.file.InMemoryModel;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.entities.FederatedIdentityEntity;
@ -209,29 +214,80 @@ public class UserAdapter implements UserModel, Comparable {
@Override
public void updateCredential(UserCredentialModel cred) {
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
updatePasswordCredential(cred);
} else {
CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType());
if (credentialEntity == null) {
credentialEntity = new CredentialEntity();
credentialEntity.setType(cred.getType());
credentialEntity.setDevice(cred.getDevice());
credentialEntity = setCredentials(user, cred);
credentialEntity.setValue(cred.getValue());
user.getCredentials().add(credentialEntity);
} else {
credentialEntity.setValue(cred.getValue());
}
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
byte[] salt = Pbkdf2PasswordEncoder.getSalt();
}
}
private void updatePasswordCredential(UserCredentialModel cred) {
CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType());
if (credentialEntity == null) {
credentialEntity = setCredentials(user, cred);
setValue(credentialEntity, cred);
user.getCredentials().add(credentialEntity);
} else {
int expiredPasswordsPolicyValue = -1;
PasswordPolicy policy = realm.getPasswordPolicy();
if(policy != null) {
expiredPasswordsPolicyValue = policy.getExpiredPasswords();
}
if (expiredPasswordsPolicyValue != -1) {
user.getCredentials().remove(credentialEntity);
credentialEntity.setType(UserCredentialModel.PASSWORD_HISTORY);
user.getCredentials().add(credentialEntity);
List<CredentialEntity> credentialEntities = getCredentialEntities(user, UserCredentialModel.PASSWORD_HISTORY);
if (credentialEntities.size() > expiredPasswordsPolicyValue - 1) {
user.getCredentials().removeAll(credentialEntities.subList(expiredPasswordsPolicyValue - 1, credentialEntities.size()));
}
credentialEntity = setCredentials(user, cred);
setValue(credentialEntity, cred);
user.getCredentials().add(credentialEntity);
} else {
List<CredentialEntity> credentialEntities = getCredentialEntities(user, UserCredentialModel.PASSWORD_HISTORY);
if (credentialEntities != null && credentialEntities.size() > 0) {
user.getCredentials().removeAll(credentialEntities);
}
setValue(credentialEntity, cred);
}
}
}
private CredentialEntity setCredentials(UserEntity user, UserCredentialModel cred) {
CredentialEntity credentialEntity = new CredentialEntity();
credentialEntity.setType(cred.getType());
credentialEntity.setCreatedDate(new Date().getTime());
credentialEntity.setDevice(cred.getDevice());
return credentialEntity;
}
private void setValue(CredentialEntity credentialEntity, UserCredentialModel cred) {
byte[] salt = getSalt();
int hashIterations = 1;
PasswordPolicy policy = realm.getPasswordPolicy();
if (policy != null) {
hashIterations = policy.getHashIterations();
if (hashIterations == -1) hashIterations = 1;
if (hashIterations == -1)
hashIterations = 1;
}
credentialEntity.setValue(new Pbkdf2PasswordEncoder(salt).encode(cred.getValue(), hashIterations));
credentialEntity.setSalt(salt);
credentialEntity.setHashIterations(hashIterations);
} else {
credentialEntity.setValue(cred.getValue());
}
credentialEntity.setDevice(cred.getDevice());
}
private CredentialEntity getCredentialEntity(UserEntity userEntity, String credType) {
@ -244,6 +300,30 @@ public class UserAdapter implements UserModel, Comparable {
return null;
}
private List<CredentialEntity> getCredentialEntities(UserEntity userEntity, String credType) {
List<CredentialEntity> credentialEntities = new ArrayList<CredentialEntity>();
for (CredentialEntity entity : userEntity.getCredentials()) {
if (entity.getType().equals(credType)) {
credentialEntities.add(entity);
}
}
// Avoiding direct use of credSecond.getCreatedDate() - credFirst.getCreatedDate() to prevent Integer Overflow
// Orders from most recent to least recent
Collections.sort(credentialEntities, new Comparator<CredentialEntity>() {
public int compare(CredentialEntity credFirst, CredentialEntity credSecond) {
if (credFirst.getCreatedDate() > credSecond.getCreatedDate()) {
return -1;
} else if (credFirst.getCreatedDate() < credSecond.getCreatedDate()) {
return 1;
} else {
return 0;
}
}
});
return credentialEntities;
}
@Override
public List<UserCredentialValueModel> getCredentialsDirectly() {
List<CredentialEntity> credentials = new ArrayList<CredentialEntity>(user.getCredentials());
@ -253,6 +333,7 @@ public class UserAdapter implements UserModel, Comparable {
UserCredentialValueModel credModel = new UserCredentialValueModel();
credModel.setType(credEntity.getType());
credModel.setDevice(credEntity.getDevice());
credModel.setCreatedDate(credEntity.getCreatedDate());
credModel.setValue(credEntity.getValue());
credModel.setSalt(credEntity.getSalt());
credModel.setHashIterations(credEntity.getHashIterations());
@ -272,6 +353,7 @@ public class UserAdapter implements UserModel, Comparable {
// credentialEntity.setId(KeycloakModelUtils.generateId());
credentialEntity.setType(credModel.getType());
// credentialEntity.setUser(user);
credModel.setCreatedDate(credModel.getCreatedDate());
user.getCredentials().add(credentialEntity);
}

View file

@ -18,7 +18,11 @@ import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
@ -160,7 +164,6 @@ public class UserAdapter implements UserModel {
}
}
@Override
public String getFirstName() {
return user.getFirstName();
@ -208,33 +211,86 @@ public class UserAdapter implements UserModel {
@Override
public void updateCredential(UserCredentialModel cred) {
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
updatePasswordCredential(cred);
} else {
CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType());
if (credentialEntity == null) {
credentialEntity = new CredentialEntity();
credentialEntity.setId(KeycloakModelUtils.generateId());
credentialEntity.setType(cred.getType());
credentialEntity.setDevice(cred.getDevice());
credentialEntity.setUser(user);
credentialEntity = setCredentials(user, cred);
credentialEntity.setValue(cred.getValue());
em.persist(credentialEntity);
user.getCredentials().add(credentialEntity);
} else {
credentialEntity.setValue(cred.getValue());
}
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
}
em.flush();
}
private void updatePasswordCredential(UserCredentialModel cred) {
CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType());
if (credentialEntity == null) {
credentialEntity = setCredentials(user, cred);
setValue(credentialEntity, cred);
em.persist(credentialEntity);
user.getCredentials().add(credentialEntity);
} else {
int expiredPasswordsPolicyValue = -1;
PasswordPolicy policy = realm.getPasswordPolicy();
if(policy != null) {
expiredPasswordsPolicyValue = policy.getExpiredPasswords();
}
if (expiredPasswordsPolicyValue != -1) {
user.getCredentials().remove(credentialEntity);
credentialEntity.setType(UserCredentialModel.PASSWORD_HISTORY);
user.getCredentials().add(credentialEntity);
List<CredentialEntity> credentialEntities = getCredentialEntities(user, UserCredentialModel.PASSWORD_HISTORY);
if (credentialEntities.size() > expiredPasswordsPolicyValue - 1) {
user.getCredentials().removeAll(credentialEntities.subList(expiredPasswordsPolicyValue - 1, credentialEntities.size()));
}
credentialEntity = setCredentials(user, cred);
setValue(credentialEntity, cred);
em.persist(credentialEntity);
user.getCredentials().add(credentialEntity);
} else {
List<CredentialEntity> credentialEntities = getCredentialEntities(user, UserCredentialModel.PASSWORD_HISTORY);
if (credentialEntities != null && credentialEntities.size() > 0) {
user.getCredentials().removeAll(credentialEntities);
}
setValue(credentialEntity, cred);
}
}
}
private CredentialEntity setCredentials(UserEntity user, UserCredentialModel cred) {
CredentialEntity credentialEntity = new CredentialEntity();
credentialEntity.setId(KeycloakModelUtils.generateId());
credentialEntity.setType(cred.getType());
credentialEntity.setCreatedDate(new Date().getTime());
credentialEntity.setDevice(cred.getDevice());
credentialEntity.setUser(user);
return credentialEntity;
}
private void setValue(CredentialEntity credentialEntity, UserCredentialModel cred) {
byte[] salt = getSalt();
int hashIterations = 1;
PasswordPolicy policy = realm.getPasswordPolicy();
if (policy != null) {
hashIterations = policy.getHashIterations();
if (hashIterations == -1) hashIterations = 1;
if (hashIterations == -1)
hashIterations = 1;
}
credentialEntity.setValue(new Pbkdf2PasswordEncoder(salt).encode(cred.getValue(), hashIterations));
credentialEntity.setSalt(salt);
credentialEntity.setHashIterations(hashIterations);
} else {
credentialEntity.setValue(cred.getValue());
}
credentialEntity.setDevice(cred.getDevice());
em.flush();
}
private CredentialEntity getCredentialEntity(UserEntity userEntity, String credType) {
@ -247,6 +303,30 @@ public class UserAdapter implements UserModel {
return null;
}
private List<CredentialEntity> getCredentialEntities(UserEntity userEntity, String credType) {
List<CredentialEntity> credentialEntities = new ArrayList<CredentialEntity>();
for (CredentialEntity entity : userEntity.getCredentials()) {
if (entity.getType().equals(credType)) {
credentialEntities.add(entity);
}
}
// Avoiding direct use of credSecond.getCreatedDate() - credFirst.getCreatedDate() to prevent Integer Overflow
// Orders from most recent to least recent
Collections.sort(credentialEntities, new Comparator<CredentialEntity>() {
public int compare(CredentialEntity credFirst, CredentialEntity credSecond) {
if (credFirst.getCreatedDate() > credSecond.getCreatedDate()) {
return -1;
} else if (credFirst.getCreatedDate() < credSecond.getCreatedDate()) {
return 1;
} else {
return 0;
}
}
});
return credentialEntities;
}
@Override
public List<UserCredentialValueModel> getCredentialsDirectly() {
List<CredentialEntity> credentials = new ArrayList<CredentialEntity>(user.getCredentials());
@ -258,6 +338,7 @@ public class UserAdapter implements UserModel {
credModel.setType(credEntity.getType());
credModel.setDevice(credEntity.getDevice());
credModel.setValue(credEntity.getValue());
credModel.setCreatedDate(credEntity.getCreatedDate());
credModel.setSalt(credEntity.getSalt());
credModel.setHashIterations(credEntity.getHashIterations());
@ -276,6 +357,7 @@ public class UserAdapter implements UserModel {
credentialEntity = new CredentialEntity();
credentialEntity.setId(KeycloakModelUtils.generateId());
credentialEntity.setType(credModel.getType());
credentialEntity.setCreatedDate(credModel.getCreatedDate());
credentialEntity.setUser(user);
em.persist(credentialEntity);
user.getCredentials().add(credentialEntity);

View file

@ -37,6 +37,8 @@ public class CredentialEntity {
protected byte[] salt;
@Column(name="HASH_ITERATIONS")
protected int hashIterations;
@Column(name="CREATED_DATE")
protected long createdDate;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="USER_ID")
@ -97,4 +99,13 @@ public class CredentialEntity {
public void setHashIterations(int hashIterations) {
this.hashIterations = hashIterations;
}
public long getCreatedDate() {
return createdDate;
}
public void setCreatedDate(long createdDate) {
this.createdDate = createdDate;
}
}

View file

@ -1,5 +1,7 @@
package org.keycloak.models.mongo.keycloak.adapters;
import static org.keycloak.models.utils.Pbkdf2PasswordEncoder.getSalt;
import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
@ -17,6 +19,8 @@ import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@ -177,31 +181,81 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
@Override
public void updateCredential(UserCredentialModel cred) {
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
updatePasswordCredential(cred);
} else {
CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType());
if (credentialEntity == null) {
credentialEntity = new CredentialEntity();
credentialEntity.setType(cred.getType());
credentialEntity.setDevice(cred.getDevice());
credentialEntity = setCredentials(user, cred);
credentialEntity.setValue(cred.getValue());
user.getCredentials().add(credentialEntity);
} else {
credentialEntity.setValue(cred.getValue());
}
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
byte[] salt = Pbkdf2PasswordEncoder.getSalt();
}
getMongoStore().updateEntity(user, invocationContext);
}
private void updatePasswordCredential(UserCredentialModel cred) {
CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType());
if (credentialEntity == null) {
credentialEntity = setCredentials(user, cred);
setValue(credentialEntity, cred);
user.getCredentials().add(credentialEntity);
} else {
int expiredPasswordsPolicyValue = -1;
PasswordPolicy policy = realm.getPasswordPolicy();
if(policy != null) {
expiredPasswordsPolicyValue = policy.getExpiredPasswords();
}
if (expiredPasswordsPolicyValue != -1) {
user.getCredentials().remove(credentialEntity);
credentialEntity.setType(UserCredentialModel.PASSWORD_HISTORY);
user.getCredentials().add(credentialEntity);
List<CredentialEntity> credentialEntities = getCredentialEntities(user, UserCredentialModel.PASSWORD_HISTORY);
if (credentialEntities.size() > expiredPasswordsPolicyValue - 1) {
user.getCredentials().removeAll(credentialEntities.subList(expiredPasswordsPolicyValue - 1, credentialEntities.size()));
}
credentialEntity = setCredentials(user, cred);
setValue(credentialEntity, cred);
user.getCredentials().add(credentialEntity);
} else {
List<CredentialEntity> credentialEntities = getCredentialEntities(user, UserCredentialModel.PASSWORD_HISTORY);
if (credentialEntities != null && credentialEntities.size() > 0) {
user.getCredentials().removeAll(credentialEntities);
}
setValue(credentialEntity, cred);
}
}
}
private CredentialEntity setCredentials(MongoUserEntity user, UserCredentialModel cred) {
CredentialEntity credentialEntity = new CredentialEntity();
credentialEntity.setType(cred.getType());
credentialEntity.setCreatedDate(new Date().getTime());
credentialEntity.setDevice(cred.getDevice());
return credentialEntity;
}
private void setValue(CredentialEntity credentialEntity, UserCredentialModel cred) {
byte[] salt = getSalt();
int hashIterations = 1;
PasswordPolicy policy = realm.getPasswordPolicy();
if (policy != null) {
hashIterations = policy.getHashIterations();
if (hashIterations == -1) hashIterations = 1;
if (hashIterations == -1)
hashIterations = 1;
}
credentialEntity.setValue(new Pbkdf2PasswordEncoder(salt).encode(cred.getValue(), hashIterations));
credentialEntity.setSalt(salt);
credentialEntity.setHashIterations(hashIterations);
} else {
credentialEntity.setValue(cred.getValue());
}
credentialEntity.setDevice(cred.getDevice());
getMongoStore().updateEntity(user, invocationContext);
}
private CredentialEntity getCredentialEntity(MongoUserEntity userEntity, String credType) {
@ -214,6 +268,30 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
return null;
}
private List<CredentialEntity> getCredentialEntities(MongoUserEntity userEntity, String credType) {
List<CredentialEntity> credentialEntities = new ArrayList<CredentialEntity>();
for (CredentialEntity entity : userEntity.getCredentials()) {
if (entity.getType().equals(credType)) {
credentialEntities.add(entity);
}
}
// Avoiding direct use of credSecond.getCreatedDate() - credFirst.getCreatedDate() to prevent Integer Overflow
// Orders from most recent to least recent
Collections.sort(credentialEntities, new Comparator<CredentialEntity>() {
public int compare(CredentialEntity credFirst, CredentialEntity credSecond) {
if (credFirst.getCreatedDate() > credSecond.getCreatedDate()) {
return -1;
} else if (credFirst.getCreatedDate() < credSecond.getCreatedDate()) {
return 1;
} else {
return 0;
}
}
});
return credentialEntities;
}
@Override
public List<UserCredentialValueModel> getCredentialsDirectly() {
List<CredentialEntity> credentials = user.getCredentials();
@ -222,6 +300,7 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
UserCredentialValueModel credModel = new UserCredentialValueModel();
credModel.setType(credEntity.getType());
credModel.setDevice(credEntity.getDevice());
credModel.setCreatedDate(credEntity.getCreatedDate());
credModel.setValue(credEntity.getValue());
credModel.setSalt(credEntity.getSalt());
credModel.setHashIterations(credEntity.getHashIterations());
@ -239,6 +318,7 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
if (credentialEntity == null) {
credentialEntity = new CredentialEntity();
credentialEntity.setType(credModel.getType());
credModel.setCreatedDate(credModel.getCreatedDate());
user.getCredentials().add(credentialEntity);
}

View file

@ -657,6 +657,8 @@ public class UsersResource {
UserCredentialModel cred = RepresentationToModel.convertCredential(pass);
try {
session.users().updateCredential(realm, user, cred);
} catch (IllegalStateException ise) {
throw new BadRequestException("Resetting to N old passwords is not allowed.");
} catch (ModelReadOnlyException mre) {
throw new BadRequestException("Can't reset password as account is read only");
}

View file

@ -229,7 +229,7 @@ public class AccountTest {
}
@Test
public void changePasswordWithPasswordPolicy() {
public void changePasswordWithLengthPasswordPolicy() {
keycloakRule.update(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
@ -263,6 +263,55 @@ public class AccountTest {
}
}
@Test
public void changePasswordWithPasswordHistoryPolicy() {
keycloakRule.update(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
appRealm.setPasswordPolicy(new PasswordPolicy("passwordHistory(2)"));
}
});
try {
changePasswordPage.open();
loginPage.login("test-user@localhost", "password");
events.expectLogin().client("account").detail(Details.REDIRECT_URI, ACCOUNT_REDIRECT + "?path=password").assertEvent();
changePasswordPage.changePassword("password", "password", "password");
Assert.assertEquals("Invalid password password: must not be equal to any of last password history.", profilePage.getError());
changePasswordPage.changePassword("password", "password1", "password1");
Assert.assertEquals("Your password has been updated.", profilePage.getSuccess());
events.expectAccount(EventType.UPDATE_PASSWORD).assertEvent();
changePasswordPage.changePassword("password1", "password", "password");
Assert.assertEquals("Invalid password password: must not be equal to any of last password history.", profilePage.getError());
changePasswordPage.changePassword("password1", "password1", "password1");
Assert.assertEquals("Invalid password password1: must not be equal to any of last password history.", profilePage.getError());
changePasswordPage.changePassword("password1", "password2", "password2");
Assert.assertEquals("Your password has been updated.", profilePage.getSuccess());
events.expectAccount(EventType.UPDATE_PASSWORD).assertEvent();
} finally {
keycloakRule.update(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
appRealm.setPasswordPolicy(new PasswordPolicy(null));
}
});
}
}
@Test
public void changeProfile() {
profilePage.open();

View file

@ -37,7 +37,6 @@ import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.MailUtil;
import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.Retry;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.pages.ErrorPage;
@ -57,8 +56,7 @@ import javax.mail.internet.MimeMessage;
import java.io.IOException;
import java.util.Collections;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.*;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -241,6 +239,44 @@ public class ResetPasswordTest {
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
}
private void resetPassword(String username, String password) throws IOException, MessagingException {
loginPage.open();
loginPage.resetPassword();
resetPasswordPage.assertCurrent();
resetPasswordPage.changePassword(username);
resetPasswordPage.assertCurrent();
String sessionId = events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).user(userId)
.detail(Details.USERNAME, username).detail(Details.EMAIL, "login@test.com").assertEvent().getSessionId();
assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage());
MimeMessage message = greenMail.getReceivedMessages()[greenMail.getReceivedMessages().length - 1];
String body = (String) message.getContent();
String changePasswordUrl = MailUtil.getLink(body);
driver.navigate().to(changePasswordUrl.trim());
updatePasswordPage.assertCurrent();
updatePasswordPage.changePassword(password, password);
events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId).session(sessionId)
.detail(Details.USERNAME, username).assertEvent();
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
events.expectLogin().user(userId).detail(Details.USERNAME, username).session(sessionId).assertEvent();
oauth.openLogout();
events.expectLogout(sessionId).user(userId).session(sessionId).assertEvent();
}
@Test
public void resetPasswordWrongEmail() throws IOException, MessagingException, InterruptedException {
loginPage.open();
@ -405,7 +441,7 @@ public class ResetPasswordTest {
}
@Test
public void resetPasswordWithPasswordPolicy() throws IOException, MessagingException {
public void resetPasswordWithLengthPasswordPolicy() throws IOException, MessagingException {
keycloakRule.update(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
@ -462,6 +498,65 @@ public class ResetPasswordTest {
events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent();
}
@Test
public void resetPasswordWithPasswordHisoryPolicy() throws IOException, MessagingException {
keycloakRule.update(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
//Block passwords that are equal to previous passwords. Default value is 3.
appRealm.setPasswordPolicy(new PasswordPolicy("passwordHistory"));
}
});
// try-catch blocks have been commented out to reduce execution time for this test case(30s->15s).
// TODO : Comment out any other piece of code, if applicable, in order to reduce execution time.
resetPassword("login-test", "password1");
/*try {
resetPassword("login-test", "password1");
fail("Expected NullPointerException: Block passwords that are equal to previous passwords.");
} catch (Exception e) {
// Expected NPE as "password1" matches with password history
}*/
resetPassword("login-test", "password2");
/*try {
resetPassword("login-test", "password1");
fail("Expected NullPointerException: Block passwords that are equal to previous passwords.");
} catch (Exception e) {
// Expected NPE as "password1" matches with password history
}
try {
resetPassword("login-test", "password2");
fail("Expected NullPointerException: Block passwords that are equal to previous passwords.");
} catch (Exception e) {
// Expected NPE as "password2" matches with password history
}*/
resetPassword("login-test", "password3");
try {
resetPassword("login-test", "password1");
fail("Expected NullPointerException: Block passwords that are equal to previous passwords.");
} catch (Exception e) {
// Expected NPE as "password1" matches with password history
}
try {
resetPassword("login-test", "password2");
fail("Expected NullPointerException: Block passwords that are equal to previous passwords.");
} catch (Exception e) {
// Expected NPE as "password2" matches with password history
}
try {
resetPassword("login-test", "password3");
fail("Expected NullPointerException: Block passwords that are equal to previous passwords.");
} catch (Exception e) {
// Expected NPE as "password3" matches with password history
}
resetPassword("login-test", "password");
}
@Test
public void resetPasswordNewBrowserSession() throws IOException, MessagingException {
String username = "login-test";