[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"/>
</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

@ -93,8 +93,10 @@ 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.
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: muss nicht gleich einem der letzten {0} Kennw\u00F6rter sein.
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: must not be equal to any of last {0} passwords.
locale_de=German
locale_en=English

View file

@ -46,12 +46,14 @@ accountDisabledMessage=Conta desativada, contate administrador
doLogOutAllSessions=Sair de todas sess\u00F5es
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\: não deve ser igual a qualquer um dos últimos {0} senhas.
locale_de=Deutsch
locale_en=English

View file

@ -900,14 +900,15 @@ module.factory('PasswordPolicy', function() {
var p = {};
p.policyMessages = {
hashIterations: "Number of hashing iterations. Default is 1. Recommended is 50000.",
length: "Minimal password length (integer type). Default value is 8.",
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.",
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)."
hashIterations: "Number of hashing iterations. Default is 1. Recommended is 50000.",
length: "Minimal password length (integer type). Default value is 8.",
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.",
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).",
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\: muss nicht gleich einem der letzten {0} Kennw\u00F6rter sein.
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: must not be equal to any of last {0} passwords.
failedToProcessResponseMessage=Failed to process response
httpsRequiredMessage=HTTPS required

View file

@ -120,11 +120,13 @@ 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
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\: não deve ser igual a qualquer um dos últimos {0} senhas.
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 {
@ -131,9 +165,15 @@ public class PasswordPolicy {
public HashIterations(String[] args) {
iterations = intArg(NAME, 1, args);
}
@Override
public Error validate(String username, String password) {
public Error validate(String user, String password) {
return null;
}
@Override
public Error validate(UserModel user, String password) {
return null;
}
}
@ -148,6 +188,11 @@ public class PasswordPolicy {
public Error validate(String username, String password) {
return username.equals(password) ? new Error(INVALID_PASSWORD_NOT_USERNAME) : null;
}
@Override
public Error validate(UserModel user, String password) {
return validate(user.getUsername(), password);
}
}
private static class Length implements Policy {
@ -157,11 +202,17 @@ public class PasswordPolicy {
public Length(String[] args) {
min = intArg(NAME, 8, args);
}
@Override
public Error validate(String username, String password) {
return password.length() < min ? new Error(INVALID_PASSWORD_MIN_LENGTH_MESSAGE, min) : null;
}
@Override
public Error validate(UserModel user, String password) {
return validate(user.getUsername(), password);
}
}
private static class Digits implements Policy {
@ -171,6 +222,7 @@ public class PasswordPolicy {
public Digits(String[] args) {
min = intArg(NAME, 1, args);
}
@Override
public Error validate(String username, String password) {
@ -182,6 +234,11 @@ public class PasswordPolicy {
}
return count < min ? new Error(INVALID_PASSWORD_MIN_DIGITS_MESSAGE, min) : null;
}
@Override
public Error validate(UserModel user, String password) {
return validate(user.getUsername(), password);
}
}
private static class LowerCase implements Policy {
@ -191,7 +248,7 @@ public class PasswordPolicy {
public LowerCase(String[] args) {
min = intArg(NAME, 1, args);
}
@Override
public Error validate(String username, String password) {
int count = 0;
@ -202,6 +259,11 @@ public class PasswordPolicy {
}
return count < min ? new Error(INVALID_PASSWORD_MIN_LOWER_CASE_CHARS_MESSAGE, min) : null;
}
@Override
public Error validate(UserModel user, String password) {
return validate(user.getUsername(), password);
}
}
private static class UpperCase implements Policy {
@ -222,6 +284,11 @@ public class PasswordPolicy {
}
return count < min ? new Error(INVALID_PASSWORD_MIN_UPPER_CASE_CHARS_MESSAGE, min) : null;
}
@Override
public Error validate(UserModel user, String password) {
return validate(user.getUsername(), password);
}
}
private static class SpecialChars implements Policy {
@ -231,7 +298,7 @@ public class PasswordPolicy {
public SpecialChars(String[] args) {
min = intArg(NAME, 1, args);
}
@Override
public Error validate(String username, String password) {
int count = 0;
@ -242,6 +309,11 @@ public class PasswordPolicy {
}
return count < min ? new Error(INVALID_PASSWORD_MIN_SPECIAL_CHARS_MESSAGE, min) : null;
}
@Override
public Error validate(UserModel user, String password) {
return validate(user.getUsername(), password);
}
}
private static class RegexPatterns implements Policy {
@ -256,17 +328,96 @@ public class PasswordPolicy {
public Error validate(String username, String password) {
Pattern pattern = null;
Matcher matcher = null;
for(String regexPattern : regexPatterns) {
for (String regexPattern : regexPatterns) {
pattern = Pattern.compile(regexPattern);
matcher = pattern.matcher(password);
if (!matcher.matches()) {
return new Error(INVALID_PASSWORD_REGEX_PATTERN, (Object)regexPatterns);
return new Error(INVALID_PASSWORD_REGEX_PATTERN, (Object) regexPatterns);
}
}
return null;
}
@Override
public Error validate(UserModel user, String password) {
return validate(user.getUsername(), password);
}
}
private static class PasswordHistory implements Policy {
private static final String NAME = "passwordHistory";
private int passwordHistoryPolicyValue;
public PasswordHistory(String[] args) {
passwordHistoryPolicyValue = intArg(NAME, 3, args);
}
@Override
public Error validate(String user, String password) {
return null;
}
@Override
public Error validate(UserModel user, String password) {
if (passwordHistoryPolicyValue != -1) {
UserCredentialValueModel cred = getCredentialValueModel(user, UserCredentialModel.PASSWORD);
if (cred != null) {
if(new Pbkdf2PasswordEncoder(cred.getSalt()).verify(password, cred.getValue(), cred.getHashIterations())) {
return new Error(INVALID_PASSWORD_HISTORY, password);
}
}
List<UserCredentialValueModel> passwordExpiredCredentials = getCredentialValueModels(user, passwordHistoryPolicyValue - 1,
UserCredentialModel.PASSWORD_HISTORY);
for (UserCredentialValueModel credential : passwordExpiredCredentials) {
if (new Pbkdf2PasswordEncoder(credential.getSalt()).verify(password, credential.getValue(), credential.getHashIterations())) {
return new Error(INVALID_PASSWORD_HISTORY, password);
}
}
}
return null;
}
private UserCredentialValueModel getCredentialValueModel(UserModel user, String credType) {
for (UserCredentialValueModel model : user.getCredentialsDirectly()) {
if (model.getType().equals(credType)) {
return model;
}
}
return null;
}
private List<UserCredentialValueModel> getCredentialValueModels(UserModel user, int expiredPasswordsPolicyValue,
String credType) {
List<UserCredentialValueModel> credentialModels = new ArrayList<UserCredentialValueModel>();
for (UserCredentialValueModel model : user.getCredentialsDirectly()) {
if (model.getType().equals(credType)) {
credentialModels.add(model);
}
}
Collections.sort(credentialModels, new Comparator<UserCredentialValueModel>() {
public int compare(UserCredentialValueModel credFirst, UserCredentialValueModel credSecond) {
if (credFirst.getCreatedDate() > credSecond.getCreatedDate()) {
return -1;
} else if (credFirst.getCreatedDate() < credSecond.getCreatedDate()) {
return 1;
} else {
return 0;
}
}
});
if (credentialModels.size() > expiredPasswordsPolicyValue) {
return credentialModels.subList(0, expiredPasswordsPolicyValue);
}
return credentialModels;
}
}
private static int intArg(String policy, int defaultValue, String... args) {
if (args == null || args.length == 0) {
return defaultValue;

View file

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

View file

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

View file

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

View file

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

View file

@ -16,7 +16,9 @@
*/
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.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,76 @@ 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 = 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());
if (credentialEntity == null) {
credentialEntity = new CredentialEntity();
credentialEntity.setType(cred.getType());
credentialEntity.setDevice(cred.getDevice());
credentialEntity = setCredentials(user, cred);
setValue(credentialEntity, cred);
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 {
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());
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) {
@ -244,6 +296,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 +329,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 +349,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,82 @@ 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 = 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());
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);
setValue(credentialEntity, cred);
em.persist(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 {
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());
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) {
@ -247,6 +299,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 +334,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 +353,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,7 +37,9 @@ 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")
protected UserEntity user;
@ -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;
@ -29,7 +33,7 @@ import java.util.Set;
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implements UserModel {
private final MongoUserEntity user;
private final RealmModel realm;
private final KeycloakSession session;
@ -177,31 +181,77 @@ 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 = 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());
if (credentialEntity == null) {
credentialEntity = new CredentialEntity();
credentialEntity.setType(cred.getType());
credentialEntity.setDevice(cred.getDevice());
credentialEntity = setCredentials(user, cred);
setValue(credentialEntity, cred);
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 {
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) {
@ -214,6 +264,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 +296,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 +314,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");
}