[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

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

View file

@ -128,7 +128,8 @@
<para> <para>
In the admin console, per realm, you can set up a password policy to enforce that users pick hard to guess passwords. 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, 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. 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 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 resistance to dictionary attacks. However the drawback to increasing n is that it has some cost (CPU usage, energy, delay) for

View file

@ -93,8 +93,10 @@ invalidPasswordMinLengthMessage=Ung\u00FCltiges Passwort: minimum l\u00E4nge {0}
invalidPasswordMinDigitsMessage=Ung\u00FCltiges Passwort: muss mindestens {0} Zahl(en) beinhalten. invalidPasswordMinDigitsMessage=Ung\u00FCltiges Passwort: muss mindestens {0} Zahl(en) beinhalten.
invalidPasswordMinLowerCaseCharsMessage=Ung\u00FCltiges Passwort: muss mindestens {0} Kleinbuchstaben beinhalten. invalidPasswordMinLowerCaseCharsMessage=Ung\u00FCltiges Passwort: muss mindestens {0} Kleinbuchstaben beinhalten.
invalidPasswordMinUpperCaseCharsMessage=Ung\u00FCltiges Passwort: muss mindestens {0} Grossbuchstaben beinhalten. invalidPasswordMinUpperCaseCharsMessage=Ung\u00FCltiges Passwort: muss mindestens {0} Grossbuchstaben beinhalten.
invalidPasswordMinSpecialCharsMessage=Ung\u00FCltiges Passwort\: muss mindestens {0} Spezialzeichen beinhalten. invalidPasswordMinSpecialCharsMessage=Ung\u00FCltiges Passwort: muss mindestens {0} Spezialzeichen beinhalten.
invalidPasswordNotUsernameMessage=Ung\u00FCltiges Passwort\: darf nicht gleich sein wie Benutzername. invalidPasswordNotUsernameMessage=Ung\u00FCltiges Passwort: darf nicht gleich sein wie Benutzername.
invalidPasswordRegexPatternMessage=Ung\u00FCltiges Passwort: nicht Regex-Muster (n) entsprechen.
invalidPasswordHistoryMessage=Ung\u00FCltiges Passwort: muss nicht gleich einem der letzten {0} Kennw\u00F6rter sein.
locale_de=Deutsch locale_de=Deutsch
locale_en=Englisch 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. invalidPasswordMinDigitsMessage=Invalid password: must contain at least {0} numerical digits.
invalidPasswordMinUpperCaseCharsMessage=Invalid password: must contain at least {0} upper case characters. invalidPasswordMinUpperCaseCharsMessage=Invalid password: must contain at least {0} upper case characters.
invalidPasswordMinSpecialCharsMessage=Invalid password: must contain at least {0} special 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: must not be equal to any of last {0} passwords.
locale_de=German locale_de=German
locale_en=English locale_en=English

View file

@ -46,12 +46,14 @@ accountDisabledMessage=Conta desativada, contate administrador
doLogOutAllSessions=Sair de todas sess\u00F5es doLogOutAllSessions=Sair de todas sess\u00F5es
accountTemporarilyDisabledMessage=A conta est\u00E1 temporariamente indispon\u00EDvel, contate administrador ou tente novamente mais tarde accountTemporarilyDisabledMessage=A conta est\u00E1 temporariamente indispon\u00EDvel, contate administrador ou tente novamente mais tarde
invalidPasswordMinLengthMessage=Senha inv\u00E1lida: comprimento m\u00EDnimo {0} invalidPasswordMinLengthMessage=Senha inv\u00E1lida\: comprimento m\u00EDnimo {0}
invalidPasswordMinLowerCaseCharsMessage=Senha inv\u00E1lida: deve conter pelo menos {0} caracteres min\u00FAsculos 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 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 invalidPasswordMinUpperCaseCharsMessage=Senha inv\u00E1lida\: deve conter pelo menos {0} caracteres mai\u00FAsculos
invalidPasswordMinSpecialCharsMessage=Senha inv\u00E1lida: deve conter pelo menos {0} caracteres especiais 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 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\: não deve ser igual a qualquer um dos últimos {0} senhas.
locale_de=Deutsch locale_de=Deutsch
locale_en=English locale_en=English

View file

@ -900,14 +900,15 @@ module.factory('PasswordPolicy', function() {
var p = {}; var p = {};
p.policyMessages = { p.policyMessages = {
hashIterations: "Number of hashing iterations. Default is 1. Recommended is 50000.", hashIterations: "Number of hashing iterations. Default is 1. Recommended is 50000.",
length: "Minimal password length (integer type). Default value is 8.", length: "Minimal password length (integer type). Default value is 8.",
digits: "Minimal number (integer type) of digits in password. Default value is 1.", digits: "Minimal number (integer type) of digits in password. Default value is 1.",
lowerCase: "Minimal number (integer type) of lowercase characters in password. Default value is 1.", lowerCase: "Minimal number (integer type) of lowercase characters in password. Default value is 1.",
upperCase: "Minimal number (integer type) of uppercase characters in password. Default value is 1.", 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.", specialChars: "Minimal number (integer type) of special characters in password. Default value is 1.",
notUsername: "Block passwords that are equal to the username", 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 = [ p.allPolicies = [
@ -918,7 +919,8 @@ module.factory('PasswordPolicy', function() {
{ name: 'upperCase', value: 1 }, { name: 'upperCase', value: 1 },
{ name: 'specialChars', value: 1 }, { name: 'specialChars', value: 1 },
{ name: 'notUsername', value: 1 }, { name: 'notUsername', value: 1 },
{ name: 'regexPatterns', value: ''} { name: 'regexPatterns', value: ''},
{ name: 'passwordHistory', value: 3 }
]; ];
p.parse = function(policyString) { p.parse = function(policyString) {

View file

@ -125,12 +125,14 @@ accountPasswordUpdatedMessage=Ihr Passwort wurde aktualisiert.
noAccessMessage=Kein Zugriff noAccessMessage=Kein Zugriff
invalidPasswordMinLengthMessage=Ung\u00FCltiges Passwort: minimum l\u00E4nge {0}. invalidPasswordMinLengthMessage=Ung\u00FCltiges Passwort\: minimum l\u00E4nge {0}.
invalidPasswordMinDigitsMessage=Ung\u00FCltiges Passwort: muss mindestens {0} Zahl(en) beinhalten. invalidPasswordMinDigitsMessage=Ung\u00FCltiges Passwort\: muss mindestens {0} Zahl(en) beinhalten.
invalidPasswordMinLowerCaseCharsMessage=Ung\u00FCltiges Passwort: muss mindestens {0} Kleinbuchstaben beinhalten. invalidPasswordMinLowerCaseCharsMessage=Ung\u00FCltiges Passwort\: muss mindestens {0} Kleinbuchstaben beinhalten.
invalidPasswordMinUpperCaseCharsMessage=Ung\u00FCltiges Passwort: muss mindestens {0} Grossbuchstaben beinhalten. invalidPasswordMinUpperCaseCharsMessage=Ung\u00FCltiges Passwort\: muss mindestens {0} Grossbuchstaben beinhalten.
invalidPasswordMinSpecialCharsMessage=Ung\u00FCltiges Passwort: muss mindestens {0} Spezialzeichen beinhalten. invalidPasswordMinSpecialCharsMessage=Ung\u00FCltiges Passwort\: muss mindestens {0} Spezialzeichen beinhalten.
invalidPasswordNotUsernameMessage=Ung\u00FCltiges Passwort\: darf nicht gleich sein wie Benutzername. invalidPasswordNotUsernameMessage=Ung\u00FCltiges Passwort\: darf nicht gleich sein wie Benutzername.
invalidPasswordRegexPatternMessage=Ung\u00FCltiges Passwort\: nicht Regex-Muster (n) entsprechen.
invalidPasswordHistoryMessage=Ung\u00FCltiges Passwort\: muss nicht gleich einem der letzten {0} Kennw\u00F6rter sein.
failedToProcessResponseMessage=Konnte Response nicht verarbeiten. failedToProcessResponseMessage=Konnte Response nicht verarbeiten.
httpsRequiredMessage=HTTPS erforderlich. 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. invalidPasswordMinLowerCaseCharsMessage=Invalid password: must contain at least {0} lower case characters.
invalidPasswordMinUpperCaseCharsMessage=Invalid password: must contain at least {0} upper case characters. invalidPasswordMinUpperCaseCharsMessage=Invalid password: must contain at least {0} upper case characters.
invalidPasswordMinSpecialCharsMessage=Invalid password: must contain at least {0} special 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: must not be equal to any of last {0} passwords.
failedToProcessResponseMessage=Failed to process response failedToProcessResponseMessage=Failed to process response
httpsRequiredMessage=HTTPS required httpsRequiredMessage=HTTPS required

View file

@ -120,11 +120,13 @@ accountPasswordUpdatedMessage=Sua senha foi atualizada
noAccessMessage=Sem acesso noAccessMessage=Sem acesso
invalidPasswordMinLengthMessage=Senha inv\u00E1lida: comprimento m\u00EDnimo {0} invalidPasswordMinLengthMessage=Senha inv\u00E1lida: comprimento m\u00EDnimo {0}
invalidPasswordMinDigitsMessage=Senha inv\u00E1lida: deve conter pelo menos {0} d\u00EDgitos num\u00E9ricos 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 invalidPasswordMinLowerCaseCharsMessage=Senha inv\u00E1lida\: deve conter pelo menos {0} caracteres min\u00FAsculos
invalidPasswordMinUpperCaseCharsMessage=Senha inv\u00E1lida: deve conter pelo menos {0} caracteres mai\u00FAsculos invalidPasswordMinUpperCaseCharsMessage=Senha inv\u00E1lida\: deve conter pelo menos {0} caracteres mai\u00FAsculos
invalidPasswordMinSpecialCharsMessage=Senha inv\u00E1lida: deve conter pelo menos {0} caracteres especiais 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 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\: não deve ser igual a qualquer um dos últimos {0} senhas.
failedToProcessResponseMessage=Falha ao processar a resposta failedToProcessResponseMessage=Falha ao processar a resposta
httpsRequiredMessage=HTTPS requerido httpsRequiredMessage=HTTPS requerido

View file

@ -1,11 +1,15 @@
package org.keycloak.models; package org.keycloak.models;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @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_MIN_SPECIAL_CHARS_MESSAGE = "invalidPasswordMinSpecialCharsMessage";
public static final String INVALID_PASSWORD_NOT_USERNAME = "invalidPasswordNotUsernameMessage"; public static final String INVALID_PASSWORD_NOT_USERNAME = "invalidPasswordNotUsernameMessage";
public static final String INVALID_PASSWORD_REGEX_PATTERN = "invalidPasswordRegexPatternMessage"; public static final String INVALID_PASSWORD_REGEX_PATTERN = "invalidPasswordRegexPatternMessage";
public static final String INVALID_PASSWORD_HISTORY = "invalidPasswordHistoryMessage";
private List<Policy> policies; private List<Policy> policies;
private String policyString; private String policyString;
@ -67,10 +72,12 @@ public class PasswordPolicy {
} else if (name.equals(HashIterations.NAME)) { } else if (name.equals(HashIterations.NAME)) {
list.add(new HashIterations(args)); list.add(new HashIterations(args));
} else if (name.equals(RegexPatterns.NAME)) { } else if (name.equals(RegexPatterns.NAME)) {
for(String regexPattern : args) { for (String regexPattern : args) {
Pattern.compile(regexPattern); Pattern.compile(regexPattern);
} }
list.add(new RegexPatterns(args)); list.add(new RegexPatterns(args));
} else if (name.equals(PasswordHistory.NAME)) {
list.add(new PasswordHistory(args));
} }
} }
return list; return list;
@ -92,9 +99,35 @@ public class PasswordPolicy {
return -1; 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) { 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) { if (error != null) {
return error; return error;
} }
@ -103,7 +136,8 @@ public class PasswordPolicy {
} }
private static interface Policy { 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 { public static class Error {
@ -131,9 +165,15 @@ public class PasswordPolicy {
public HashIterations(String[] args) { public HashIterations(String[] args) {
iterations = intArg(NAME, 1, args); iterations = intArg(NAME, 1, args);
} }
@Override @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; return null;
} }
} }
@ -148,6 +188,11 @@ public class PasswordPolicy {
public Error validate(String username, String password) { public Error validate(String username, String password) {
return username.equals(password) ? new Error(INVALID_PASSWORD_NOT_USERNAME) : null; 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 { private static class Length implements Policy {
@ -157,11 +202,17 @@ public class PasswordPolicy {
public Length(String[] args) { public Length(String[] args) {
min = intArg(NAME, 8, args); min = intArg(NAME, 8, args);
} }
@Override @Override
public Error validate(String username, String password) { public Error validate(String username, String password) {
return password.length() < min ? new Error(INVALID_PASSWORD_MIN_LENGTH_MESSAGE, min) : null; 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 { private static class Digits implements Policy {
@ -171,6 +222,7 @@ public class PasswordPolicy {
public Digits(String[] args) { public Digits(String[] args) {
min = intArg(NAME, 1, args); min = intArg(NAME, 1, args);
} }
@Override @Override
public Error validate(String username, String password) { 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; 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 { private static class LowerCase implements Policy {
@ -191,7 +248,7 @@ public class PasswordPolicy {
public LowerCase(String[] args) { public LowerCase(String[] args) {
min = intArg(NAME, 1, args); min = intArg(NAME, 1, args);
} }
@Override @Override
public Error validate(String username, String password) { public Error validate(String username, String password) {
int count = 0; int count = 0;
@ -202,6 +259,11 @@ public class PasswordPolicy {
} }
return count < min ? new Error(INVALID_PASSWORD_MIN_LOWER_CASE_CHARS_MESSAGE, min) : null; 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 { 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; 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 { private static class SpecialChars implements Policy {
@ -231,7 +298,7 @@ public class PasswordPolicy {
public SpecialChars(String[] args) { public SpecialChars(String[] args) {
min = intArg(NAME, 1, args); min = intArg(NAME, 1, args);
} }
@Override @Override
public Error validate(String username, String password) { public Error validate(String username, String password) {
int count = 0; int count = 0;
@ -242,6 +309,11 @@ public class PasswordPolicy {
} }
return count < min ? new Error(INVALID_PASSWORD_MIN_SPECIAL_CHARS_MESSAGE, min) : null; 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 { private static class RegexPatterns implements Policy {
@ -256,17 +328,96 @@ public class PasswordPolicy {
public Error validate(String username, String password) { public Error validate(String username, String password) {
Pattern pattern = null; Pattern pattern = null;
Matcher matcher = null; Matcher matcher = null;
for(String regexPattern : regexPatterns) { for (String regexPattern : regexPatterns) {
pattern = Pattern.compile(regexPattern); pattern = Pattern.compile(regexPattern);
matcher = pattern.matcher(password); matcher = pattern.matcher(password);
if (!matcher.matches()) { if (!matcher.matches()) {
return new Error(INVALID_PASSWORD_REGEX_PATTERN, (Object)regexPatterns); return new Error(INVALID_PASSWORD_REGEX_PATTERN, (Object) regexPatterns);
} }
} }
return null; 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) { private static int intArg(String policy, int defaultValue, String... args) {
if (args == null || args.length == 0) { if (args == null || args.length == 0) {
return defaultValue; return defaultValue;

View file

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

View file

@ -12,6 +12,7 @@ public class UserCredentialValueModel {
private String device; private String device;
private byte[] salt; private byte[] salt;
private int hashIterations; private int hashIterations;
private long createdDate;
public String getType() { public String getType() {
return type; return type;
@ -52,4 +53,13 @@ public class UserCredentialValueModel {
public void setHashIterations(int iterations) { public void setHashIterations(int iterations) {
this.hashIterations = 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) { public void updateCredential(RealmModel realm, UserModel user, UserCredentialModel credential) {
if (credential.getType().equals(UserCredentialModel.PASSWORD)) { if (credential.getType().equals(UserCredentialModel.PASSWORD)) {
if (realm.getPasswordPolicy() != null) { 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()); if (error != null) throw new ModelException(error.getMessage(), error.getParameters());
} }
} }

View file

@ -5,11 +5,23 @@ package org.keycloak.models.entities;
*/ */
public class CredentialEntity { public class CredentialEntity {
private String id;
private String type; private String type;
private String value; private String value;
private String device; private String device;
private byte[] salt; private byte[] salt;
private int hashIterations; 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() { public String getType() {
return type; return type;
@ -50,4 +62,21 @@ public class CredentialEntity {
public void setHashIterations(int hashIterations) { public void setHashIterations(int hashIterations) {
this.hashIterations = 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

@ -16,7 +16,9 @@
*/ */
package org.keycloak.models.file.adapter; package org.keycloak.models.file.adapter;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import static org.keycloak.models.utils.Pbkdf2PasswordEncoder.getSalt;
import org.keycloak.models.PasswordPolicy; import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
@ -28,11 +30,14 @@ import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import org.keycloak.connections.file.InMemoryModel; import org.keycloak.connections.file.InMemoryModel;
import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.entities.FederatedIdentityEntity; import org.keycloak.models.entities.FederatedIdentityEntity;
@ -209,29 +214,76 @@ public class UserAdapter implements UserModel, Comparable {
@Override @Override
public void updateCredential(UserCredentialModel cred) { public void updateCredential(UserCredentialModel cred) {
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
updatePasswordCredential(cred);
} else {
CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType());
if (credentialEntity == null) {
credentialEntity = setCredentials(user, cred);
credentialEntity.setValue(cred.getValue());
user.getCredentials().add(credentialEntity);
} else {
credentialEntity.setValue(cred.getValue());
}
}
}
private void updatePasswordCredential(UserCredentialModel cred) {
CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType()); CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType());
if (credentialEntity == null) { if (credentialEntity == null) {
credentialEntity = new CredentialEntity(); credentialEntity = setCredentials(user, cred);
credentialEntity.setType(cred.getType()); setValue(credentialEntity, cred);
credentialEntity.setDevice(cred.getDevice());
user.getCredentials().add(credentialEntity); user.getCredentials().add(credentialEntity);
}
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
byte[] salt = Pbkdf2PasswordEncoder.getSalt();
int hashIterations = 1;
PasswordPolicy policy = realm.getPasswordPolicy();
if (policy != null) {
hashIterations = policy.getHashIterations();
if (hashIterations == -1) hashIterations = 1;
}
credentialEntity.setValue(new Pbkdf2PasswordEncoder(salt).encode(cred.getValue(), hashIterations));
credentialEntity.setSalt(salt);
credentialEntity.setHashIterations(hashIterations);
} else { } else {
credentialEntity.setValue(cred.getValue());
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 {
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()); 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;
}
credentialEntity.setValue(new Pbkdf2PasswordEncoder(salt).encode(cred.getValue(), hashIterations));
credentialEntity.setSalt(salt);
credentialEntity.setHashIterations(hashIterations);
} }
private CredentialEntity getCredentialEntity(UserEntity userEntity, String credType) { private CredentialEntity getCredentialEntity(UserEntity userEntity, String credType) {
@ -244,6 +296,30 @@ public class UserAdapter implements UserModel, Comparable {
return null; 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 @Override
public List<UserCredentialValueModel> getCredentialsDirectly() { public List<UserCredentialValueModel> getCredentialsDirectly() {
List<CredentialEntity> credentials = new ArrayList<CredentialEntity>(user.getCredentials()); List<CredentialEntity> credentials = new ArrayList<CredentialEntity>(user.getCredentials());
@ -253,6 +329,7 @@ public class UserAdapter implements UserModel, Comparable {
UserCredentialValueModel credModel = new UserCredentialValueModel(); UserCredentialValueModel credModel = new UserCredentialValueModel();
credModel.setType(credEntity.getType()); credModel.setType(credEntity.getType());
credModel.setDevice(credEntity.getDevice()); credModel.setDevice(credEntity.getDevice());
credModel.setCreatedDate(credEntity.getCreatedDate());
credModel.setValue(credEntity.getValue()); credModel.setValue(credEntity.getValue());
credModel.setSalt(credEntity.getSalt()); credModel.setSalt(credEntity.getSalt());
credModel.setHashIterations(credEntity.getHashIterations()); credModel.setHashIterations(credEntity.getHashIterations());
@ -272,6 +349,7 @@ public class UserAdapter implements UserModel, Comparable {
// credentialEntity.setId(KeycloakModelUtils.generateId()); // credentialEntity.setId(KeycloakModelUtils.generateId());
credentialEntity.setType(credModel.getType()); credentialEntity.setType(credModel.getType());
// credentialEntity.setUser(user); // credentialEntity.setUser(user);
credModel.setCreatedDate(credModel.getCreatedDate());
user.getCredentials().add(credentialEntity); user.getCredentials().add(credentialEntity);
} }

View file

@ -18,7 +18,11 @@ import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
import javax.persistence.EntityManager; import javax.persistence.EntityManager;
import javax.persistence.TypedQuery; import javax.persistence.TypedQuery;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
@ -160,7 +164,6 @@ public class UserAdapter implements UserModel {
} }
} }
@Override @Override
public String getFirstName() { public String getFirstName() {
return user.getFirstName(); return user.getFirstName();
@ -208,33 +211,82 @@ public class UserAdapter implements UserModel {
@Override @Override
public void updateCredential(UserCredentialModel cred) { public void updateCredential(UserCredentialModel cred) {
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
updatePasswordCredential(cred);
} else {
CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType());
if (credentialEntity == null) {
credentialEntity = setCredentials(user, cred);
credentialEntity.setValue(cred.getValue());
em.persist(credentialEntity);
user.getCredentials().add(credentialEntity);
} else {
credentialEntity.setValue(cred.getValue());
}
}
em.flush();
}
private void updatePasswordCredential(UserCredentialModel cred) {
CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType()); CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType());
if (credentialEntity == null) { if (credentialEntity == null) {
credentialEntity = new CredentialEntity(); credentialEntity = setCredentials(user, cred);
credentialEntity.setId(KeycloakModelUtils.generateId()); setValue(credentialEntity, cred);
credentialEntity.setType(cred.getType());
credentialEntity.setDevice(cred.getDevice());
credentialEntity.setUser(user);
em.persist(credentialEntity); em.persist(credentialEntity);
user.getCredentials().add(credentialEntity); user.getCredentials().add(credentialEntity);
}
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
byte[] salt = getSalt();
int hashIterations = 1;
PasswordPolicy policy = realm.getPasswordPolicy();
if (policy != null) {
hashIterations = policy.getHashIterations();
if (hashIterations == -1) hashIterations = 1;
}
credentialEntity.setValue(new Pbkdf2PasswordEncoder(salt).encode(cred.getValue(), hashIterations));
credentialEntity.setSalt(salt);
credentialEntity.setHashIterations(hashIterations);
} else { } else {
credentialEntity.setValue(cred.getValue());
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 {
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.setDevice(cred.getDevice());
em.flush(); 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;
}
credentialEntity.setValue(new Pbkdf2PasswordEncoder(salt).encode(cred.getValue(), hashIterations));
credentialEntity.setSalt(salt);
credentialEntity.setHashIterations(hashIterations);
} }
private CredentialEntity getCredentialEntity(UserEntity userEntity, String credType) { private CredentialEntity getCredentialEntity(UserEntity userEntity, String credType) {
@ -247,6 +299,30 @@ public class UserAdapter implements UserModel {
return null; 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 @Override
public List<UserCredentialValueModel> getCredentialsDirectly() { public List<UserCredentialValueModel> getCredentialsDirectly() {
List<CredentialEntity> credentials = new ArrayList<CredentialEntity>(user.getCredentials()); List<CredentialEntity> credentials = new ArrayList<CredentialEntity>(user.getCredentials());
@ -258,6 +334,7 @@ public class UserAdapter implements UserModel {
credModel.setType(credEntity.getType()); credModel.setType(credEntity.getType());
credModel.setDevice(credEntity.getDevice()); credModel.setDevice(credEntity.getDevice());
credModel.setValue(credEntity.getValue()); credModel.setValue(credEntity.getValue());
credModel.setCreatedDate(credEntity.getCreatedDate());
credModel.setSalt(credEntity.getSalt()); credModel.setSalt(credEntity.getSalt());
credModel.setHashIterations(credEntity.getHashIterations()); credModel.setHashIterations(credEntity.getHashIterations());
@ -276,6 +353,7 @@ public class UserAdapter implements UserModel {
credentialEntity = new CredentialEntity(); credentialEntity = new CredentialEntity();
credentialEntity.setId(KeycloakModelUtils.generateId()); credentialEntity.setId(KeycloakModelUtils.generateId());
credentialEntity.setType(credModel.getType()); credentialEntity.setType(credModel.getType());
credentialEntity.setCreatedDate(credModel.getCreatedDate());
credentialEntity.setUser(user); credentialEntity.setUser(user);
em.persist(credentialEntity); em.persist(credentialEntity);
user.getCredentials().add(credentialEntity); user.getCredentials().add(credentialEntity);

View file

@ -37,7 +37,9 @@ public class CredentialEntity {
protected byte[] salt; protected byte[] salt;
@Column(name="HASH_ITERATIONS") @Column(name="HASH_ITERATIONS")
protected int hashIterations; protected int hashIterations;
@Column(name="CREATED_DATE")
protected long createdDate;
@ManyToOne(fetch = FetchType.LAZY) @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="USER_ID") @JoinColumn(name="USER_ID")
protected UserEntity user; protected UserEntity user;
@ -97,4 +99,13 @@ public class CredentialEntity {
public void setHashIterations(int hashIterations) { public void setHashIterations(int hashIterations) {
this.hashIterations = 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; 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.connections.mongo.api.context.MongoStoreInvocationContext;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
@ -17,6 +19,8 @@ import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@ -29,7 +33,7 @@ import java.util.Set;
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */
public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implements UserModel { public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implements UserModel {
private final MongoUserEntity user; private final MongoUserEntity user;
private final RealmModel realm; private final RealmModel realm;
private final KeycloakSession session; private final KeycloakSession session;
@ -177,31 +181,77 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
@Override @Override
public void updateCredential(UserCredentialModel cred) { public void updateCredential(UserCredentialModel cred) {
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
updatePasswordCredential(cred);
} else {
CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType());
if (credentialEntity == null) {
credentialEntity = setCredentials(user, cred);
credentialEntity.setValue(cred.getValue());
user.getCredentials().add(credentialEntity);
} else {
credentialEntity.setValue(cred.getValue());
}
}
getMongoStore().updateEntity(user, invocationContext);
}
private void updatePasswordCredential(UserCredentialModel cred) {
CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType()); CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType());
if (credentialEntity == null) { if (credentialEntity == null) {
credentialEntity = new CredentialEntity(); credentialEntity = setCredentials(user, cred);
credentialEntity.setType(cred.getType()); setValue(credentialEntity, cred);
credentialEntity.setDevice(cred.getDevice());
user.getCredentials().add(credentialEntity); user.getCredentials().add(credentialEntity);
}
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
byte[] salt = Pbkdf2PasswordEncoder.getSalt();
int hashIterations = 1;
PasswordPolicy policy = realm.getPasswordPolicy();
if (policy != null) {
hashIterations = policy.getHashIterations();
if (hashIterations == -1) hashIterations = 1;
}
credentialEntity.setValue(new Pbkdf2PasswordEncoder(salt).encode(cred.getValue(), hashIterations));
credentialEntity.setSalt(salt);
credentialEntity.setHashIterations(hashIterations);
} else { } else {
credentialEntity.setValue(cred.getValue());
}
credentialEntity.setDevice(cred.getDevice());
getMongoStore().updateEntity(user, invocationContext); 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 {
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;
}
credentialEntity.setValue(new Pbkdf2PasswordEncoder(salt).encode(cred.getValue(), hashIterations));
credentialEntity.setSalt(salt);
credentialEntity.setHashIterations(hashIterations);
} }
private CredentialEntity getCredentialEntity(MongoUserEntity userEntity, String credType) { private CredentialEntity getCredentialEntity(MongoUserEntity userEntity, String credType) {
@ -214,6 +264,30 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
return null; 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 @Override
public List<UserCredentialValueModel> getCredentialsDirectly() { public List<UserCredentialValueModel> getCredentialsDirectly() {
List<CredentialEntity> credentials = user.getCredentials(); List<CredentialEntity> credentials = user.getCredentials();
@ -222,6 +296,7 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
UserCredentialValueModel credModel = new UserCredentialValueModel(); UserCredentialValueModel credModel = new UserCredentialValueModel();
credModel.setType(credEntity.getType()); credModel.setType(credEntity.getType());
credModel.setDevice(credEntity.getDevice()); credModel.setDevice(credEntity.getDevice());
credModel.setCreatedDate(credEntity.getCreatedDate());
credModel.setValue(credEntity.getValue()); credModel.setValue(credEntity.getValue());
credModel.setSalt(credEntity.getSalt()); credModel.setSalt(credEntity.getSalt());
credModel.setHashIterations(credEntity.getHashIterations()); credModel.setHashIterations(credEntity.getHashIterations());
@ -239,6 +314,7 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
if (credentialEntity == null) { if (credentialEntity == null) {
credentialEntity = new CredentialEntity(); credentialEntity = new CredentialEntity();
credentialEntity.setType(credModel.getType()); credentialEntity.setType(credModel.getType());
credModel.setCreatedDate(credModel.getCreatedDate());
user.getCredentials().add(credentialEntity); user.getCredentials().add(credentialEntity);
} }

View file

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