diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.RC1.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.RC1.xml index 34771683a6..aa78acbdd6 100755 --- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.RC1.xml +++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.RC1.xml @@ -32,6 +32,9 @@ + + + diff --git a/docbook/reference/en/en-US/modules/security-vulnerabilities.xml b/docbook/reference/en/en-US/modules/security-vulnerabilities.xml index 359c55e5f3..0756ebdfc4 100755 --- a/docbook/reference/en/en-US/modules/security-vulnerabilities.xml +++ b/docbook/reference/en/en-US/modules/security-vulnerabilities.xml @@ -128,7 +128,8 @@ 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 diff --git a/forms/common-themes/src/main/resources/theme/base/account/messages/messages_de.properties b/forms/common-themes/src/main/resources/theme/base/account/messages/messages_de.properties index 61456927eb..b3b388f45f 100644 --- a/forms/common-themes/src/main/resources/theme/base/account/messages/messages_de.properties +++ b/forms/common-themes/src/main/resources/theme/base/account/messages/messages_de.properties @@ -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 diff --git a/forms/common-themes/src/main/resources/theme/base/account/messages/messages_en.properties b/forms/common-themes/src/main/resources/theme/base/account/messages/messages_en.properties index 48a51aa807..713904bc9a 100755 --- a/forms/common-themes/src/main/resources/theme/base/account/messages/messages_en.properties +++ b/forms/common-themes/src/main/resources/theme/base/account/messages/messages_en.properties @@ -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 diff --git a/forms/common-themes/src/main/resources/theme/base/account/messages/messages_pt_BR.properties b/forms/common-themes/src/main/resources/theme/base/account/messages/messages_pt_BR.properties index bf3144e681..4af23551d5 100644 --- a/forms/common-themes/src/main/resources/theme/base/account/messages/messages_pt_BR.properties +++ b/forms/common-themes/src/main/resources/theme/base/account/messages/messages_pt_BR.properties @@ -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 diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js index da0b2eb1a7..df05a13ffc 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js @@ -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) { diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_de.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_de.properties index 75ffd3ee9b..53e46a8d63 100644 --- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_de.properties +++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_de.properties @@ -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. diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties index 7c1347f389..11833e580f 100755 --- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties +++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties @@ -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 diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties index 58dba8442b..bd0c37c33d 100644 --- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties +++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties @@ -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 diff --git a/model/api/src/main/java/org/keycloak/models/PasswordPolicy.java b/model/api/src/main/java/org/keycloak/models/PasswordPolicy.java index 703e9c6593..a47aa9b0c8 100755 --- a/model/api/src/main/java/org/keycloak/models/PasswordPolicy.java +++ b/model/api/src/main/java/org/keycloak/models/PasswordPolicy.java @@ -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 Stian Thorgersen */ @@ -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 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 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 getCredentialValueModels(UserModel user, int expiredPasswordsPolicyValue, + String credType) { + List credentialModels = new ArrayList(); + for (UserCredentialValueModel model : user.getCredentialsDirectly()) { + if (model.getType().equals(credType)) { + credentialModels.add(model); + } + } + + Collections.sort(credentialModels, new Comparator() { + 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; diff --git a/model/api/src/main/java/org/keycloak/models/UserCredentialModel.java b/model/api/src/main/java/org/keycloak/models/UserCredentialModel.java index 5fb60050eb..083eea85e0 100755 --- a/model/api/src/main/java/org/keycloak/models/UserCredentialModel.java +++ b/model/api/src/main/java/org/keycloak/models/UserCredentialModel.java @@ -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 diff --git a/model/api/src/main/java/org/keycloak/models/UserCredentialValueModel.java b/model/api/src/main/java/org/keycloak/models/UserCredentialValueModel.java index bd1213f32b..663eaeca19 100755 --- a/model/api/src/main/java/org/keycloak/models/UserCredentialValueModel.java +++ b/model/api/src/main/java/org/keycloak/models/UserCredentialValueModel.java @@ -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; + } + } diff --git a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java index 1016341e3a..94d0226aca 100755 --- a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java +++ b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java @@ -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()); } } diff --git a/model/api/src/main/java/org/keycloak/models/entities/CredentialEntity.java b/model/api/src/main/java/org/keycloak/models/entities/CredentialEntity.java index 6255836a40..5493fe24d5 100755 --- a/model/api/src/main/java/org/keycloak/models/entities/CredentialEntity.java +++ b/model/api/src/main/java/org/keycloak/models/entities/CredentialEntity.java @@ -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; + } + } diff --git a/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java index e9ba84ab01..5e66376306 100755 --- a/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java +++ b/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java @@ -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 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 getCredentialEntities(UserEntity userEntity, String credType) { + List credentialEntities = new ArrayList(); + 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() { + 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 getCredentialsDirectly() { List credentials = new ArrayList(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); } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java index a51d0de21b..62ca898161 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java @@ -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 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 getCredentialEntities(UserEntity userEntity, String credType) { + List credentialEntities = new ArrayList(); + 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() { + 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 getCredentialsDirectly() { List credentials = new ArrayList(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); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/CredentialEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/CredentialEntity.java index de4039c9f4..bdbfe84f42 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/CredentialEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/CredentialEntity.java @@ -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; + } + } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java index 874f7d8159..afc99e94d0 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java @@ -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 Marek Posolda */ public class UserAdapter extends AbstractMongoAdapter implements UserModel { - + private final MongoUserEntity user; private final RealmModel realm; private final KeycloakSession session; @@ -177,31 +181,77 @@ public class UserAdapter extends AbstractMongoAdapter 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 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 implement return null; } + private List getCredentialEntities(MongoUserEntity userEntity, String credType) { + List credentialEntities = new ArrayList(); + 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() { + 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 getCredentialsDirectly() { List credentials = user.getCredentials(); @@ -222,6 +296,7 @@ public class UserAdapter extends AbstractMongoAdapter 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 implement if (credentialEntity == null) { credentialEntity = new CredentialEntity(); credentialEntity.setType(credModel.getType()); + credModel.setCreatedDate(credModel.getCreatedDate()); user.getCredentials().add(credentialEntity); } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java index 8bad8982ef..bbf202cd82 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java @@ -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"); }