keycloak-memoizeit
Changes
core/src/main/java/org/keycloak/representations/idm/PasswordPolicyTypeRepresentation.java 70(+70 -0)
server-spi/src/main/java/org/keycloak/policy/DefaultPasswordPolicyManagerProviderFactory.java 51(+51 -0)
server-spi/src/main/java/org/keycloak/policy/ForceExpiredPasswordPolicyProviderFactory.java 90(+90 -0)
server-spi/src/main/java/org/keycloak/policy/HashAlgorithmPasswordPolicyProviderFactory.java 91(+91 -0)
server-spi/src/main/java/org/keycloak/policy/HashIterationsPasswordPolicyProviderFactory.java 91(+91 -0)
server-spi/src/main/java/org/keycloak/policy/NotUsernamePasswordPolicyProviderFactory.java 73(+73 -0)
server-spi/src/main/java/org/keycloak/policy/RegexPatternsPasswordPolicyProviderFactory.java 73(+73 -0)
server-spi/src/main/java/org/keycloak/policy/SpecialCharsPasswordPolicyProviderFactory.java 73(+73 -0)
server-spi/src/main/resources/META-INF/services/org.keycloak.policy.PasswordPolicyManagerProviderFactory 18(+18 -0)
server-spi/src/main/resources/META-INF/services/org.keycloak.policy.PasswordPolicyProviderFactory 28(+28 -0)
services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java 24(+20 -4)
testsuite/integration/src/test/java/org/keycloak/testsuite/model/PasswordPolicyTest.java 185(+185 -0)
Details
diff --git a/core/src/main/java/org/keycloak/representations/idm/PasswordPolicyTypeRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/PasswordPolicyTypeRepresentation.java
new file mode 100644
index 0000000..5a5a1f9
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/idm/PasswordPolicyTypeRepresentation.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.representations.idm;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class PasswordPolicyTypeRepresentation {
+
+ private String id;
+ private String displayName;
+ private String configType;
+ private String defaultValue;
+ private boolean multipleSupported;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ public void setDisplayName(String displayName) {
+ this.displayName = displayName;
+ }
+
+ public String getConfigType() {
+ return configType;
+ }
+
+ public void setConfigType(String configType) {
+ this.configType = configType;
+ }
+
+ public String getDefaultValue() {
+ return defaultValue;
+ }
+
+ public void setDefaultValue(String defaultValue) {
+ this.defaultValue = defaultValue;
+ }
+
+ public boolean isMultipleSupported() {
+ return multipleSupported;
+ }
+
+ public void setMultipleSupported(boolean multipleSupported) {
+ this.multipleSupported = multipleSupported;
+ }
+}
diff --git a/core/src/main/java/org/keycloak/representations/info/ServerInfoRepresentation.java b/core/src/main/java/org/keycloak/representations/info/ServerInfoRepresentation.java
index 5510bb2..c3aeeeb 100755
--- a/core/src/main/java/org/keycloak/representations/info/ServerInfoRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/info/ServerInfoRepresentation.java
@@ -17,6 +17,7 @@
package org.keycloak.representations.info;
+import org.keycloak.representations.idm.PasswordPolicyTypeRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.idm.ProtocolMapperTypeRepresentation;
@@ -43,6 +44,8 @@ public class ServerInfoRepresentation {
private Map<String, List<ProtocolMapperRepresentation>> builtinProtocolMappers;
private Map<String, List<ClientInstallationRepresentation>> clientInstallations;
+ private List<PasswordPolicyTypeRepresentation> passwordPolicies;
+
private Map<String, List<String>> enums;
public SystemInfoRepresentation getSystemInfo() {
@@ -132,4 +135,13 @@ public class ServerInfoRepresentation {
public void setClientInstallations(Map<String, List<ClientInstallationRepresentation>> clientInstallations) {
this.clientInstallations = clientInstallations;
}
+
+ public List<PasswordPolicyTypeRepresentation> getPasswordPolicies() {
+ return passwordPolicies;
+ }
+
+ public void setPasswordPolicies(List<PasswordPolicyTypeRepresentation> passwordPolicies) {
+ this.passwordPolicies = passwordPolicies;
+ }
+
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index 40112cd..6d7ea98 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -1223,7 +1223,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
@Override
public PasswordPolicy getPasswordPolicy() {
if (passwordPolicy == null) {
- passwordPolicy = new PasswordPolicy(realm.getPasswordPolicy());
+ passwordPolicy = PasswordPolicy.parse(session, realm.getPasswordPolicy());
}
return passwordPolicy;
}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
index c532cbd..5c9c589 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
@@ -308,7 +308,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
@Override
public PasswordPolicy getPasswordPolicy() {
if (passwordPolicy == null) {
- passwordPolicy = new PasswordPolicy(realm.getPasswordPolicy());
+ passwordPolicy = PasswordPolicy.parse(session, realm.getPasswordPolicy());
}
return passwordPolicy;
}
diff --git a/server-spi/src/main/java/org/keycloak/hash/PasswordHashManager.java b/server-spi/src/main/java/org/keycloak/hash/PasswordHashManager.java
index 8f77d60..9c5afa8 100644
--- a/server-spi/src/main/java/org/keycloak/hash/PasswordHashManager.java
+++ b/server-spi/src/main/java/org/keycloak/hash/PasswordHashManager.java
@@ -18,7 +18,11 @@
package org.keycloak.hash;
import org.jboss.logging.Logger;
-import org.keycloak.models.*;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.PasswordPolicy;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserCredentialValueModel;
+import org.keycloak.policy.HashAlgorithmPasswordPolicyProviderFactory;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -32,17 +36,12 @@ public class PasswordHashManager {
}
public static UserCredentialValueModel encode(KeycloakSession session, PasswordPolicy passwordPolicy, String rawPassword) {
- String algorithm = passwordPolicy.getHashAlgorithm();
- int iterations = passwordPolicy.getHashIterations();
- if (iterations < 1) {
- iterations = 1;
- }
PasswordHashProvider provider = session.getProvider(PasswordHashProvider.class, passwordPolicy.getHashAlgorithm());
if (provider == null) {
- log.warnv("Could not find hash provider {0} from password policy, using default provider {1}", algorithm, Constants.DEFAULT_HASH_ALGORITHM);
- provider = session.getProvider(PasswordHashProvider.class, Constants.DEFAULT_HASH_ALGORITHM);
+ log.warnv("Could not find hash provider {0} from password policy, using default provider {1}", passwordPolicy.getHashAlgorithm(), HashAlgorithmPasswordPolicyProviderFactory.DEFAULT_VALUE);
+ provider = session.getProvider(PasswordHashProvider.class, HashAlgorithmPasswordPolicyProviderFactory.DEFAULT_VALUE);
}
- return provider.encode(rawPassword, iterations);
+ return provider.encode(rawPassword, passwordPolicy.getHashIterations());
}
public static boolean verify(KeycloakSession session, RealmModel realm, String password, UserCredentialValueModel credential) {
diff --git a/server-spi/src/main/java/org/keycloak/models/Constants.java b/server-spi/src/main/java/org/keycloak/models/Constants.java
index 7f998df..916565a 100755
--- a/server-spi/src/main/java/org/keycloak/models/Constants.java
+++ b/server-spi/src/main/java/org/keycloak/models/Constants.java
@@ -41,8 +41,6 @@ public interface Constants {
String AUTHZ_UMA_AUTHORIZATION = "uma_authorization";
String[] AUTHZ_DEFAULT_AUTHORIZATION_ROLES = {AUTHZ_UMA_AUTHORIZATION};
- String DEFAULT_HASH_ALGORITHM = "pbkdf2";
-
// 15 minutes
int DEFAULT_ACCESS_TOKEN_LIFESPAN_FOR_IMPLICIT_FLOW_TIMEOUT = 900;
// 30 days
diff --git a/server-spi/src/main/java/org/keycloak/models/PasswordPolicy.java b/server-spi/src/main/java/org/keycloak/models/PasswordPolicy.java
index 0b22b4e..95dfc53 100755
--- a/server-spi/src/main/java/org/keycloak/models/PasswordPolicy.java
+++ b/server-spi/src/main/java/org/keycloak/models/PasswordPolicy.java
@@ -17,498 +17,101 @@
package org.keycloak.models;
-import org.keycloak.hash.PasswordHashManager;
+import org.keycloak.policy.ForceExpiredPasswordPolicyProviderFactory;
+import org.keycloak.policy.HashAlgorithmPasswordPolicyProviderFactory;
+import org.keycloak.policy.HashIterationsPasswordPolicyProviderFactory;
+import org.keycloak.policy.HistoryPasswordPolicyProviderFactory;
+import org.keycloak.policy.PasswordPolicyProvider;
import java.io.Serializable;
-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 java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class PasswordPolicy implements Serializable {
- public static final String INVALID_PASSWORD_MIN_LENGTH_MESSAGE = "invalidPasswordMinLengthMessage";
- public static final String INVALID_PASSWORD_MIN_DIGITS_MESSAGE = "invalidPasswordMinDigitsMessage";
- public static final String INVALID_PASSWORD_MIN_LOWER_CASE_CHARS_MESSAGE = "invalidPasswordMinLowerCaseCharsMessage";
- public static final String INVALID_PASSWORD_MIN_UPPER_CASE_CHARS_MESSAGE = "invalidPasswordMinUpperCaseCharsMessage";
- 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;
+ private Map<String, Object> policyConfig;
- public PasswordPolicy(String policyString) {
- this.policyString = policyString;
- this.policies = new LinkedList<>();
+ public static PasswordPolicy empty() {
+ return new PasswordPolicy(null, new HashMap<>());
+ }
+
+ public static PasswordPolicy parse(KeycloakSession session, String policyString) {
+ Map<String, Object> policyConfig = new HashMap<>();
if (policyString != null && !policyString.trim().isEmpty()) {
for (String policy : policyString.split(" and ")) {
policy = policy.trim();
- String name;
- String arg = null;
+ String key;
+ String config = null;
int i = policy.indexOf('(');
if (i == -1) {
- name = policy.trim();
+ key = policy.trim();
} else {
- name = policy.substring(0, i).trim();
- arg = policy.substring(i + 1, policy.length() - 1);
+ key = policy.substring(0, i).trim();
+ config = policy.substring(i + 1, policy.length() - 1);
}
- if (name.equals(Length.NAME)) {
- policies.add(new Length(arg));
- } else if (name.equals(Digits.NAME)) {
- policies.add(new Digits(arg));
- } else if (name.equals(LowerCase.NAME)) {
- policies.add(new LowerCase(arg));
- } else if (name.equals(UpperCase.NAME)) {
- policies.add(new UpperCase(arg));
- } else if (name.equals(SpecialChars.NAME)) {
- policies.add(new SpecialChars(arg));
- } else if (name.equals(NotUsername.NAME)) {
- policies.add(new NotUsername(arg));
- } else if (name.equals(HashAlgorithm.NAME)) {
- policies.add(new HashAlgorithm(arg));
- } else if (name.equals(HashIterations.NAME)) {
- policies.add(new HashIterations(arg));
- } else if (name.equals(RegexPatterns.NAME)) {
- Pattern.compile(arg);
- policies.add(new RegexPatterns(arg));
- } else if (name.equals(PasswordHistory.NAME)) {
- policies.add(new PasswordHistory(arg, this));
- } else if (name.equals(ForceExpiredPasswordChange.NAME)) {
- policies.add(new ForceExpiredPasswordChange(arg));
- } else {
+ PasswordPolicyProvider provider = session.getProvider(PasswordPolicyProvider.class, key);
+ if (provider == null) {
throw new IllegalArgumentException("Unsupported policy");
}
- }
- }
- }
-
- public String getHashAlgorithm() {
- if (policies == null)
- return Constants.DEFAULT_HASH_ALGORITHM;
- for (Policy p : policies) {
- if (p instanceof HashAlgorithm) {
- return ((HashAlgorithm) p).algorithm;
- }
-
- }
- return Constants.DEFAULT_HASH_ALGORITHM;
- }
- /**
- *
- * @return -1 if no hash iterations setting
- */
- public int getHashIterations() {
- if (policies == null)
- return -1;
- for (Policy p : policies) {
- if (p instanceof HashIterations) {
- return ((HashIterations) p).iterations;
+ policyConfig.put(key, provider.parseConfig(config));
}
-
- }
- return -1;
- }
-
- /**
- *
- * @return -1 if no expired passwords setting
- */
- public int getExpiredPasswords() {
- if (policies == null)
- return -1;
- for (Policy p : policies) {
- if (p instanceof PasswordHistory) {
- return ((PasswordHistory) p).passwordHistoryPolicyValue;
- }
-
- }
- return -1;
- }
-
- /**
- *
- * @return -1 if no force expired password change setting
- */
- public int getDaysToExpirePassword() {
- if (policies == null)
- return -1;
- for (Policy p : policies) {
- if (p instanceof ForceExpiredPasswordChange) {
- return ((ForceExpiredPasswordChange) p).daysToExpirePassword;
- }
-
- }
- return -1;
- }
-
- public Error validate(KeycloakSession session, UserModel user, String password) {
- for (Policy p : policies) {
- Error error = p.validate(session, user, password);
- if (error != null) {
- return error;
- }
- }
- return null;
- }
-
- public Error validate(KeycloakSession session, String user, String password) {
- for (Policy p : policies) {
- Error error = p.validate(session, user, password);
- if (error != null) {
- return error;
- }
- }
- return null;
- }
-
- private static interface Policy extends Serializable {
- public Error validate(KeycloakSession session, UserModel user, String password);
- public Error validate(KeycloakSession session, String user, String password);
- }
-
- public static class Error {
- private String message;
- private Object[] parameters;
-
- private Error(String message, Object... parameters) {
- this.message = message;
- this.parameters = parameters;
- }
-
- public String getMessage() {
- return message;
}
- public Object[] getParameters() {
- return parameters;
- }
- }
-
- private static class HashAlgorithm implements Policy {
- private static final String NAME = "hashAlgorithm";
- private String algorithm;
-
- public HashAlgorithm(String arg) {
- algorithm = stringArg(NAME, Constants.DEFAULT_HASH_ALGORITHM, arg);
- }
-
- @Override
- public Error validate(KeycloakSession session, String user, String password) {
- return null;
- }
-
- @Override
- public Error validate(KeycloakSession session, UserModel user, String password) {
- return null;
- }
+ return new PasswordPolicy(policyString, policyConfig);
}
- private static class HashIterations implements Policy {
- private static final String NAME = "hashIterations";
- private int iterations;
-
- public HashIterations(String arg) {
- iterations = intArg(NAME, 1, arg);
- }
-
- @Override
- public Error validate(KeycloakSession session, String user, String password) {
- return null;
- }
-
- @Override
- public Error validate(KeycloakSession session, UserModel user, String password) {
- return null;
- }
- }
-
- private static class NotUsername implements Policy {
- private static final String NAME = "notUsername";
-
- public NotUsername(String arg) {
- }
-
- @Override
- public Error validate(KeycloakSession session, String username, String password) {
- return username.equals(password) ? new Error(INVALID_PASSWORD_NOT_USERNAME) : null;
- }
-
- @Override
- public Error validate(KeycloakSession session, UserModel user, String password) {
- return validate(session, user.getUsername(), password);
- }
- }
-
- private static class Length implements Policy {
- private static final String NAME = "length";
- private int min;
-
- public Length(String arg)
- {
- min = intArg(NAME, 8, arg);
- }
-
-
- @Override
- public Error validate(KeycloakSession session, String username, String password) {
- return password.length() < min ? new Error(INVALID_PASSWORD_MIN_LENGTH_MESSAGE, min) : null;
- }
-
- @Override
- public Error validate(KeycloakSession session, UserModel user, String password) {
- return validate(session, user.getUsername(), password);
- }
- }
-
- private static class Digits implements Policy {
- private static final String NAME = "digits";
- private int min;
-
- public Digits(String arg)
- {
- min = intArg(NAME, 1, arg);
- }
-
-
- @Override
- public Error validate(KeycloakSession session, String username, String password) {
- int count = 0;
- for (char c : password.toCharArray()) {
- if (Character.isDigit(c)) {
- count++;
- }
- }
- return count < min ? new Error(INVALID_PASSWORD_MIN_DIGITS_MESSAGE, min) : null;
- }
-
- @Override
- public Error validate(KeycloakSession session, UserModel user, String password) {
- return validate(session, user.getUsername(), password);
- }
+ private PasswordPolicy(String policyString, Map<String, Object> policyConfig) {
+ this.policyString = policyString;
+ this.policyConfig = policyConfig;
}
- private static class LowerCase implements Policy {
- private static final String NAME = "lowerCase";
- private int min;
-
- public LowerCase(String arg)
- {
- min = intArg(NAME, 1, arg);
- }
-
- @Override
- public Error validate(KeycloakSession session, String username, String password) {
- int count = 0;
- for (char c : password.toCharArray()) {
- if (Character.isLowerCase(c)) {
- count++;
- }
- }
- return count < min ? new Error(INVALID_PASSWORD_MIN_LOWER_CASE_CHARS_MESSAGE, min) : null;
- }
-
- @Override
- public Error validate(KeycloakSession session, UserModel user, String password) {
- return validate(session, user.getUsername(), password);
- }
+ public Set<String> getPolicies() {
+ return policyConfig.keySet();
}
- private static class UpperCase implements Policy {
- private static final String NAME = "upperCase";
- private int min;
-
- public UpperCase(String arg) {
- min = intArg(NAME, 1, arg);
- }
-
- @Override
- public Error validate(KeycloakSession session, String username, String password) {
- int count = 0;
- for (char c : password.toCharArray()) {
- if (Character.isUpperCase(c)) {
- count++;
- }
- }
- return count < min ? new Error(INVALID_PASSWORD_MIN_UPPER_CASE_CHARS_MESSAGE, min) : null;
- }
-
- @Override
- public Error validate(KeycloakSession session, UserModel user, String password) {
- return validate(session, user.getUsername(), password);
- }
+ public <T> T getPolicyConfig(String key) {
+ return (T) policyConfig.get(key);
}
- private static class SpecialChars implements Policy {
- private static final String NAME = "specialChars";
- private int min;
-
- public SpecialChars(String arg)
- {
- min = intArg(NAME, 1, arg);
- }
-
- @Override
- public Error validate(KeycloakSession session, String username, String password) {
- int count = 0;
- for (char c : password.toCharArray()) {
- if (!Character.isLetterOrDigit(c)) {
- count++;
- }
- }
- return count < min ? new Error(INVALID_PASSWORD_MIN_SPECIAL_CHARS_MESSAGE, min) : null;
- }
-
- @Override
- public Error validate(KeycloakSession session, UserModel user, String password) {
- return validate(session, user.getUsername(), password);
- }
- }
-
- private static class RegexPatterns implements Policy {
- private static final String NAME = "regexPattern";
- private String regexPattern;
-
- public RegexPatterns(String arg)
- {
- regexPattern = arg;
- }
-
- @Override
- public Error validate(KeycloakSession session, String username, String password) {
- Pattern pattern = Pattern.compile(regexPattern);
- Matcher matcher = pattern.matcher(password);
- if (!matcher.matches()) {
- return new Error(INVALID_PASSWORD_REGEX_PATTERN, (Object) regexPattern);
- }
- return null;
- }
-
- @Override
- public Error validate(KeycloakSession session, UserModel user, String password) {
- return validate(session, user.getUsername(), password);
+ public String getHashAlgorithm() {
+ if (policyConfig.containsKey(HashAlgorithmPasswordPolicyProviderFactory.ID)) {
+ return getPolicyConfig(HashAlgorithmPasswordPolicyProviderFactory.ID);
+ } else {
+ return HashAlgorithmPasswordPolicyProviderFactory.DEFAULT_VALUE;
}
}
- private static class PasswordHistory implements Policy {
- private static final String NAME = "passwordHistory";
- private final PasswordPolicy passwordPolicy;
- private int passwordHistoryPolicyValue;
-
- public PasswordHistory(String arg, PasswordPolicy passwordPolicy)
- {
- this.passwordPolicy = passwordPolicy;
- passwordHistoryPolicyValue = intArg(NAME, 3, arg);
- }
-
- @Override
- public Error validate(KeycloakSession session, String user, String password) {
- return null;
- }
-
- @Override
- public Error validate(KeycloakSession session, UserModel user, String password) {
- if (passwordHistoryPolicyValue != -1) {
- UserCredentialValueModel cred = getCredentialValueModel(user, UserCredentialModel.PASSWORD);
- if (cred != null) {
- if(PasswordHashManager.verify(session, passwordPolicy, password, cred)) {
- return new Error(INVALID_PASSWORD_HISTORY, passwordHistoryPolicyValue);
- }
- }
-
- List<UserCredentialValueModel> passwordExpiredCredentials = getCredentialValueModels(user, passwordHistoryPolicyValue - 1,
- UserCredentialModel.PASSWORD_HISTORY);
- for (UserCredentialValueModel credential : passwordExpiredCredentials) {
- if (PasswordHashManager.verify(session, passwordPolicy, password, credential)) {
- return new Error(INVALID_PASSWORD_HISTORY, passwordHistoryPolicyValue);
- }
- }
- }
- 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;
+ public int getHashIterations() {
+ if (policyConfig.containsKey(HashIterationsPasswordPolicyProviderFactory.ID)) {
+ return getPolicyConfig(HashIterationsPasswordPolicyProviderFactory.ID);
+ } else {
+ return HashIterationsPasswordPolicyProviderFactory.DEFAULT_VALUE;
}
}
-
- private static class ForceExpiredPasswordChange implements Policy {
- private static final String NAME = "forceExpiredPasswordChange";
- private int daysToExpirePassword;
-
- public ForceExpiredPasswordChange(String arg) {
- daysToExpirePassword = intArg(NAME, 365, arg);
- }
- @Override
- public Error validate(KeycloakSession session, String username, String password) {
- return null;
- }
-
- @Override
- public Error validate(KeycloakSession session, UserModel user, String password) {
- return null;
- }
- }
-
- private static int intArg(String policy, int defaultValue, String arg) {
- if (arg == null) {
- return defaultValue;
+ public int getExpiredPasswords() {
+ if (policyConfig.containsKey(HistoryPasswordPolicyProviderFactory.ID)) {
+ return getPolicyConfig(HistoryPasswordPolicyProviderFactory.ID);
} else {
- return Integer.parseInt(arg);
+ return -1;
}
}
- private static String stringArg(String policy, String defaultValue, String arg) {
- if (arg == null) {
- return defaultValue;
+ public int getDaysToExpirePassword() {
+ if (policyConfig.containsKey(ForceExpiredPasswordPolicyProviderFactory.ID)) {
+ return getPolicyConfig(ForceExpiredPasswordPolicyProviderFactory.ID);
} else {
- return arg;
+ return -1;
}
}
@@ -516,4 +119,5 @@ public class PasswordPolicy implements Serializable {
public String toString() {
return policyString;
}
+
}
diff --git a/server-spi/src/main/java/org/keycloak/models/UserFederationManager.java b/server-spi/src/main/java/org/keycloak/models/UserFederationManager.java
index 08a070d..19db2e1 100755
--- a/server-spi/src/main/java/org/keycloak/models/UserFederationManager.java
+++ b/server-spi/src/main/java/org/keycloak/models/UserFederationManager.java
@@ -19,6 +19,8 @@ package org.keycloak.models;
import org.jboss.logging.Logger;
import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.policy.PasswordPolicyManagerProvider;
+import org.keycloak.policy.PolicyError;
import org.keycloak.services.managers.UserManager;
import org.keycloak.storage.StorageProviderModel;
@@ -493,7 +495,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(session, user, credential.getValue());
+ PolicyError error = session.getProvider(PasswordPolicyManagerProvider.class).validate(user, credential.getValue());
if (error != null) throw new ModelException(error.getMessage(), error.getParameters());
}
}
diff --git a/server-spi/src/main/java/org/keycloak/models/utils/CredentialValidation.java b/server-spi/src/main/java/org/keycloak/models/utils/CredentialValidation.java
index c6b3762..85d52c0 100755
--- a/server-spi/src/main/java/org/keycloak/models/utils/CredentialValidation.java
+++ b/server-spi/src/main/java/org/keycloak/models/utils/CredentialValidation.java
@@ -17,19 +17,18 @@
package org.keycloak.models.utils;
+import org.keycloak.common.util.Time;
import org.keycloak.hash.PasswordHashManager;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OTPPolicy;
-import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.models.UserModel;
import org.keycloak.representations.PasswordToken;
-import org.keycloak.common.util.Time;
import java.util.List;
@@ -39,15 +38,6 @@ import java.util.List;
*/
public class CredentialValidation {
- private static int hashIterations(RealmModel realm) {
- PasswordPolicy policy = realm.getPasswordPolicy();
- if (policy != null) {
- return policy.getHashIterations();
- }
- return -1;
-
- }
-
/**
* Will update password if hash iteration policy has changed
*
@@ -78,8 +68,7 @@ public class CredentialValidation {
boolean validated = PasswordHashManager.verify(session, realm, unhashedCredValue, credential);
if (validated) {
- int iterations = hashIterations(realm);
- if (iterations > -1 && iterations != credential.getHashIterations()) {
+ if (realm.getPasswordPolicy().getHashIterations() != credential.getHashIterations()) {
UserCredentialValueModel newCred = PasswordHashManager.encode(session, realm, unhashedCredValue);
user.updateCredentialDirectly(newCred);
diff --git a/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
index b36b96c..3efca79 100755
--- a/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
@@ -196,7 +196,7 @@ public class RepresentationToModel {
newRealm.addRequiredCredential(CredentialRepresentation.PASSWORD);
}
- if (rep.getPasswordPolicy() != null) newRealm.setPasswordPolicy(new PasswordPolicy(rep.getPasswordPolicy()));
+ if (rep.getPasswordPolicy() != null) newRealm.setPasswordPolicy(PasswordPolicy.parse(session, rep.getPasswordPolicy()));
if (rep.getOtpPolicyType() != null) newRealm.setOTPPolicy(toPolicy(rep));
else newRealm.setOTPPolicy(OTPPolicy.DEFAULT_POLICY);
@@ -661,7 +661,7 @@ public class RepresentationToModel {
return url != null ? url.replace(target, replacement) : null;
}
- public static void updateRealm(RealmRepresentation rep, RealmModel realm) {
+ public static void updateRealm(RealmRepresentation rep, RealmModel realm, KeycloakSession session) {
if (rep.getRealm() != null) {
renameRealm(realm, rep.getRealm());
}
@@ -709,7 +709,7 @@ public class RepresentationToModel {
if (rep.isAdminEventsDetailsEnabled() != null) realm.setAdminEventsDetailsEnabled(rep.isAdminEventsDetailsEnabled());
- if (rep.getPasswordPolicy() != null) realm.setPasswordPolicy(new PasswordPolicy(rep.getPasswordPolicy()));
+ if (rep.getPasswordPolicy() != null) realm.setPasswordPolicy(PasswordPolicy.parse(session, rep.getPasswordPolicy()));
if (rep.getOtpPolicyType() != null) realm.setOTPPolicy(toPolicy(rep));
if (rep.getDefaultRoles() != null) {
diff --git a/server-spi/src/main/java/org/keycloak/policy/DefaultPasswordPolicyManagerProvider.java b/server-spi/src/main/java/org/keycloak/policy/DefaultPasswordPolicyManagerProvider.java
new file mode 100644
index 0000000..c380f80
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/policy/DefaultPasswordPolicyManagerProvider.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.policy;
+
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.PasswordPolicy;
+import org.keycloak.models.UserModel;
+
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class DefaultPasswordPolicyManagerProvider implements PasswordPolicyManagerProvider {
+
+ private KeycloakSession session;
+
+ public DefaultPasswordPolicyManagerProvider(KeycloakSession session) {
+ this.session = session;
+ }
+
+ @Override
+ public PolicyError validate(UserModel user, String password) {
+ for (PasswordPolicyProvider p : getProviders(session)) {
+ PolicyError policyError = p.validate(user, password);
+ if (policyError != null) {
+ return policyError;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public PolicyError validate(String user, String password) {
+ for (PasswordPolicyProvider p : getProviders(session)) {
+ PolicyError policyError = p.validate(user, password);
+ if (policyError != null) {
+ return policyError;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void close() {
+ }
+
+ private List<PasswordPolicyProvider> getProviders(KeycloakSession session) {
+ LinkedList<PasswordPolicyProvider> list = new LinkedList<>();
+ PasswordPolicy policy = session.getContext().getRealm().getPasswordPolicy();
+ for (String id : policy.getPolicies()) {
+ PasswordPolicyProvider provider = session.getProvider(PasswordPolicyProvider.class, id);
+ list.add(provider);
+ }
+ return list;
+ }
+
+}
diff --git a/server-spi/src/main/java/org/keycloak/policy/DefaultPasswordPolicyManagerProviderFactory.java b/server-spi/src/main/java/org/keycloak/policy/DefaultPasswordPolicyManagerProviderFactory.java
new file mode 100644
index 0000000..b8aabd4
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/policy/DefaultPasswordPolicyManagerProviderFactory.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.policy;
+
+import org.keycloak.Config;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class DefaultPasswordPolicyManagerProviderFactory implements PasswordPolicyManagerProviderFactory {
+
+ @Override
+ public PasswordPolicyManagerProvider create(KeycloakSession session) {
+ return new DefaultPasswordPolicyManagerProvider(session);
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+ }
+
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public String getId() {
+ return "default";
+ }
+
+}
diff --git a/server-spi/src/main/java/org/keycloak/policy/DigitsPasswordPolicyProvider.java b/server-spi/src/main/java/org/keycloak/policy/DigitsPasswordPolicyProvider.java
new file mode 100644
index 0000000..d3ca22d
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/policy/DigitsPasswordPolicyProvider.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.policy;
+
+import org.keycloak.models.KeycloakContext;
+import org.keycloak.models.UserModel;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class DigitsPasswordPolicyProvider implements PasswordPolicyProvider {
+
+ private static final String ERROR_MESSAGE = "invalidPasswordMinDigitsMessage";
+
+ private KeycloakContext context;
+
+ public DigitsPasswordPolicyProvider(KeycloakContext context) {
+ this.context = context;
+ }
+
+ @Override
+ public PolicyError validate(String username, String password) {
+ int min = context.getRealm().getPasswordPolicy().getPolicyConfig(DigitsPasswordPolicyProviderFactory.ID);
+ int count = 0;
+ for (char c : password.toCharArray()) {
+ if (Character.isDigit(c)) {
+ count++;
+ }
+ }
+ return count < min ? new PolicyError(ERROR_MESSAGE, min) : null;
+ }
+
+ @Override
+ public PolicyError validate(UserModel user, String password) {
+ return validate(user.getUsername(), password);
+ }
+
+ @Override
+ public Object parseConfig(String value) {
+ return value != null ? Integer.parseInt(value) : 1;
+ }
+
+ @Override
+ public void close() {
+ }
+
+}
diff --git a/server-spi/src/main/java/org/keycloak/policy/DigitsPasswordPolicyProviderFactory.java b/server-spi/src/main/java/org/keycloak/policy/DigitsPasswordPolicyProviderFactory.java
new file mode 100644
index 0000000..d7fce9c
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/policy/DigitsPasswordPolicyProviderFactory.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.policy;
+
+import org.keycloak.Config;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class DigitsPasswordPolicyProviderFactory implements PasswordPolicyProviderFactory {
+
+ static final String ID = "digits";
+
+ @Override
+ public String getId() {
+ return ID;
+ }
+
+ @Override
+ public PasswordPolicyProvider create(KeycloakSession session) {
+ return new DigitsPasswordPolicyProvider(session.getContext());
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+ }
+
+ @Override
+ public String getDisplayName() {
+ return "Digits";
+ }
+
+ @Override
+ public String getConfigType() {
+ return PasswordPolicyProvider.INT_CONFIG_TYPE;
+ }
+
+ @Override
+ public String getDefaultConfigValue() {
+ return "1";
+ }
+
+ @Override
+ public boolean isMultiplSupported() {
+ return false;
+ }
+
+ @Override
+ public void close() {
+ }
+
+}
diff --git a/server-spi/src/main/java/org/keycloak/policy/ForceExpiredPasswordPolicyProviderFactory.java b/server-spi/src/main/java/org/keycloak/policy/ForceExpiredPasswordPolicyProviderFactory.java
new file mode 100644
index 0000000..ecefffb
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/policy/ForceExpiredPasswordPolicyProviderFactory.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.policy;
+
+import org.keycloak.Config;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.UserModel;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class ForceExpiredPasswordPolicyProviderFactory implements PasswordPolicyProviderFactory, PasswordPolicyProvider {
+
+ public static final String ID = "forceExpiredPasswordChange";
+ public static final int DEFAULT_VALUE = 365;
+
+ @Override
+ public PasswordPolicyProvider create(KeycloakSession session) {
+ return this;
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+ }
+
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public String getId() {
+ return ID;
+ }
+
+ @Override
+ public PolicyError validate(UserModel user, String password) {
+ return null;
+ }
+
+ @Override
+ public PolicyError validate(String user, String password) {
+ return null;
+ }
+
+ @Override
+ public String getDisplayName() {
+ return "Expire Password";
+ }
+
+ @Override
+ public String getConfigType() {
+ return PasswordPolicyProvider.STRING_CONFIG_TYPE;
+ }
+
+ @Override
+ public String getDefaultConfigValue() {
+ return String.valueOf(DEFAULT_VALUE);
+ }
+
+ @Override
+ public boolean isMultiplSupported() {
+ return false;
+ }
+
+ @Override
+ public Object parseConfig(String value) {
+ return value != null ? Integer.parseInt(value) : DEFAULT_VALUE;
+ }
+
+}
diff --git a/server-spi/src/main/java/org/keycloak/policy/HashAlgorithmPasswordPolicyProviderFactory.java b/server-spi/src/main/java/org/keycloak/policy/HashAlgorithmPasswordPolicyProviderFactory.java
new file mode 100644
index 0000000..add3b03
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/policy/HashAlgorithmPasswordPolicyProviderFactory.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.policy;
+
+import org.keycloak.Config;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.UserModel;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class HashAlgorithmPasswordPolicyProviderFactory implements PasswordPolicyProviderFactory, PasswordPolicyProvider {
+
+ public static final String DEFAULT_VALUE = "pbkdf2";
+
+ public static final String ID = "hashAlgorithm";
+
+ @Override
+ public PasswordPolicyProvider create(KeycloakSession session) {
+ return this;
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+ }
+
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public String getId() {
+ return ID;
+ }
+
+ @Override
+ public PolicyError validate(UserModel user, String password) {
+ return null;
+ }
+
+ @Override
+ public PolicyError validate(String user, String password) {
+ return null;
+ }
+
+ @Override
+ public String getDisplayName() {
+ return "Hashing Algorithm";
+ }
+
+ @Override
+ public String getConfigType() {
+ return PasswordPolicyProvider.STRING_CONFIG_TYPE;
+ }
+
+ @Override
+ public String getDefaultConfigValue() {
+ return DEFAULT_VALUE;
+ }
+
+ @Override
+ public boolean isMultiplSupported() {
+ return false;
+ }
+
+ @Override
+ public Object parseConfig(String value) {
+ return value != null ? value : DEFAULT_VALUE;
+ }
+
+}
diff --git a/server-spi/src/main/java/org/keycloak/policy/HashIterationsPasswordPolicyProviderFactory.java b/server-spi/src/main/java/org/keycloak/policy/HashIterationsPasswordPolicyProviderFactory.java
new file mode 100644
index 0000000..64a77d5
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/policy/HashIterationsPasswordPolicyProviderFactory.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.policy;
+
+import org.keycloak.Config;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.UserModel;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class HashIterationsPasswordPolicyProviderFactory implements PasswordPolicyProvider, PasswordPolicyProviderFactory {
+
+ public static final int DEFAULT_VALUE = 20000;
+
+ public static final String ID = "hashIterations";
+
+ @Override
+ public PasswordPolicyProvider create(KeycloakSession session) {
+ return this;
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+ }
+
+ @Override
+ public String getId() {
+ return ID;
+ }
+
+ @Override
+ public PolicyError validate(UserModel user, String password) {
+ return null;
+ }
+
+ @Override
+ public PolicyError validate(String user, String password) {
+ return null;
+ }
+
+ @Override
+ public Object parseConfig(String value) {
+ return value != null ? Integer.parseInt(value) : DEFAULT_VALUE;
+ }
+
+ @Override
+ public String getDisplayName() {
+ return "Hashing Iterations";
+ }
+
+ @Override
+ public String getConfigType() {
+ return PasswordPolicyProvider.INT_CONFIG_TYPE;
+ }
+
+ @Override
+ public String getDefaultConfigValue() {
+ return String.valueOf(DEFAULT_VALUE);
+ }
+
+ @Override
+ public boolean isMultiplSupported() {
+ return false;
+ }
+
+ @Override
+ public void close() {
+ }
+
+}
diff --git a/server-spi/src/main/java/org/keycloak/policy/HistoryPasswordPolicyProvider.java b/server-spi/src/main/java/org/keycloak/policy/HistoryPasswordPolicyProvider.java
new file mode 100644
index 0000000..3cfe5fb
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/policy/HistoryPasswordPolicyProvider.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.policy;
+
+import org.keycloak.hash.PasswordHashManager;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.PasswordPolicy;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.models.UserCredentialValueModel;
+import org.keycloak.models.UserModel;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class HistoryPasswordPolicyProvider implements PasswordPolicyProvider {
+
+ private static final String ERROR_MESSAGE = "invalidPasswordHistoryMessage";
+
+ private KeycloakSession session;
+
+ public HistoryPasswordPolicyProvider(KeycloakSession session) {
+ this.session = session;
+ }
+
+ @Override
+ public PolicyError validate(String username, String password) {
+ return null;
+ }
+
+ @Override
+ public PolicyError validate(UserModel user, String password) {
+ PasswordPolicy policy = session.getContext().getRealm().getPasswordPolicy();
+ int passwordHistoryPolicyValue = policy.getPolicyConfig(HistoryPasswordPolicyProviderFactory.ID);
+ if (passwordHistoryPolicyValue != -1) {
+ UserCredentialValueModel cred = getCredentialValueModel(user, UserCredentialModel.PASSWORD);
+ if (cred != null) {
+ if(PasswordHashManager.verify(session, policy, password, cred)) {
+ return new PolicyError(ERROR_MESSAGE, passwordHistoryPolicyValue);
+ }
+ }
+
+ List<UserCredentialValueModel> passwordExpiredCredentials = getCredentialValueModels(user, passwordHistoryPolicyValue - 1,
+ UserCredentialModel.PASSWORD_HISTORY);
+ for (UserCredentialValueModel credential : passwordExpiredCredentials) {
+ if (PasswordHashManager.verify(session, policy, password, credential)) {
+ return new PolicyError(ERROR_MESSAGE, passwordHistoryPolicyValue);
+ }
+ }
+ }
+ 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;
+ }
+
+ @Override
+ public Object parseConfig(String value) {
+ return value != null ? Integer.parseInt(value) : HistoryPasswordPolicyProviderFactory.DEFAULT_VALUE;
+ }
+
+ @Override
+ public void close() {
+ }
+
+}
diff --git a/server-spi/src/main/java/org/keycloak/policy/HistoryPasswordPolicyProviderFactory.java b/server-spi/src/main/java/org/keycloak/policy/HistoryPasswordPolicyProviderFactory.java
new file mode 100644
index 0000000..c2c180a
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/policy/HistoryPasswordPolicyProviderFactory.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.policy;
+
+import org.keycloak.Config;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class HistoryPasswordPolicyProviderFactory implements PasswordPolicyProviderFactory {
+
+ public static final String ID = "passwordHistory";
+ public static final Integer DEFAULT_VALUE = 3;
+
+ @Override
+ public String getId() {
+ return ID;
+ }
+
+ @Override
+ public PasswordPolicyProvider create(KeycloakSession session) {
+ return new HistoryPasswordPolicyProvider(session);
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+ }
+
+ @Override
+ public String getDisplayName() {
+ return "Not Recently Used";
+ }
+
+ @Override
+ public String getConfigType() {
+ return PasswordPolicyProvider.INT_CONFIG_TYPE;
+ }
+
+ @Override
+ public String getDefaultConfigValue() {
+ return String.valueOf(HistoryPasswordPolicyProviderFactory.DEFAULT_VALUE);
+ }
+
+ @Override
+ public boolean isMultiplSupported() {
+ return false;
+ }
+
+ @Override
+ public void close() {
+ }
+
+}
diff --git a/server-spi/src/main/java/org/keycloak/policy/LengthPasswordPolicyProvider.java b/server-spi/src/main/java/org/keycloak/policy/LengthPasswordPolicyProvider.java
new file mode 100644
index 0000000..bdafdc9
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/policy/LengthPasswordPolicyProvider.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.policy;
+
+import org.keycloak.models.KeycloakContext;
+import org.keycloak.models.UserModel;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class LengthPasswordPolicyProvider implements PasswordPolicyProvider {
+
+ private static final String ERROR_MESSAGE = "invalidPasswordMinLengthMessage";
+
+ private KeycloakContext context;
+
+ public LengthPasswordPolicyProvider(KeycloakContext context) {
+ this.context = context;
+ }
+
+ @Override
+ public PolicyError validate(String username, String password) {
+ int min = context.getRealm().getPasswordPolicy().getPolicyConfig(LengthPasswordPolicyProviderFactory.ID);
+ return password.length() < min ? new PolicyError(ERROR_MESSAGE, min) : null;
+ }
+
+ @Override
+ public PolicyError validate(UserModel user, String password) {
+ return validate(user.getUsername(), password);
+ }
+
+ @Override
+ public Object parseConfig(String value) {
+ return value != null ? Integer.parseInt(value) : 8;
+ }
+
+ @Override
+ public void close() {
+ }
+
+}
diff --git a/server-spi/src/main/java/org/keycloak/policy/LengthPasswordPolicyProviderFactory.java b/server-spi/src/main/java/org/keycloak/policy/LengthPasswordPolicyProviderFactory.java
new file mode 100644
index 0000000..a60c250
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/policy/LengthPasswordPolicyProviderFactory.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.policy;
+
+import org.keycloak.Config;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class LengthPasswordPolicyProviderFactory implements PasswordPolicyProviderFactory {
+
+ static final String ID = "length";
+
+ @Override
+ public String getId() {
+ return ID;
+ }
+
+ @Override
+ public PasswordPolicyProvider create(KeycloakSession session) {
+ return new LengthPasswordPolicyProvider(session.getContext());
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+ }
+
+ @Override
+ public String getDisplayName() {
+ return "Minimum Length";
+ }
+
+ @Override
+ public String getConfigType() {
+ return PasswordPolicyProvider.INT_CONFIG_TYPE;
+ }
+
+ @Override
+ public String getDefaultConfigValue() {
+ return "8";
+ }
+
+ @Override
+ public boolean isMultiplSupported() {
+ return false;
+ }
+
+ @Override
+ public void close() {
+ }
+
+}
diff --git a/server-spi/src/main/java/org/keycloak/policy/LowerCasePasswordPolicyProvider.java b/server-spi/src/main/java/org/keycloak/policy/LowerCasePasswordPolicyProvider.java
new file mode 100644
index 0000000..3312e2d
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/policy/LowerCasePasswordPolicyProvider.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.policy;
+
+import org.keycloak.models.KeycloakContext;
+import org.keycloak.models.UserModel;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class LowerCasePasswordPolicyProvider implements PasswordPolicyProvider {
+
+ private static final String ERROR_MESSAGE = "invalidPasswordMinLowerCaseCharsMessage";
+
+ private KeycloakContext context;
+
+ public LowerCasePasswordPolicyProvider(KeycloakContext context) {
+ this.context = context;
+ }
+
+ @Override
+ public PolicyError validate(String username, String password) {
+ int min = context.getRealm().getPasswordPolicy().getPolicyConfig(LowerCasePasswordPolicyProviderFactory.ID);
+ int count = 0;
+ for (char c : password.toCharArray()) {
+ if (Character.isLowerCase(c)) {
+ count++;
+ }
+ }
+ return count < min ? new PolicyError(ERROR_MESSAGE, min) : null;
+ }
+
+ @Override
+ public PolicyError validate(UserModel user, String password) {
+ return validate(user.getUsername(), password);
+ }
+
+ @Override
+ public Object parseConfig(String value) {
+ return value != null ? Integer.parseInt(value) : 1;
+ }
+
+ @Override
+ public void close() {
+ }
+
+}
diff --git a/server-spi/src/main/java/org/keycloak/policy/LowerCasePasswordPolicyProviderFactory.java b/server-spi/src/main/java/org/keycloak/policy/LowerCasePasswordPolicyProviderFactory.java
new file mode 100644
index 0000000..7e96dcf
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/policy/LowerCasePasswordPolicyProviderFactory.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.policy;
+
+import org.keycloak.Config;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class LowerCasePasswordPolicyProviderFactory implements PasswordPolicyProviderFactory {
+
+ public static final String ID = "lowerCase";
+
+ @Override
+ public PasswordPolicyProvider create(KeycloakSession session) {
+ return new LowerCasePasswordPolicyProvider(session.getContext());
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+ }
+
+ @Override
+ public String getDisplayName() {
+ return "Lowercase Characters";
+ }
+
+ @Override
+ public String getConfigType() {
+ return PasswordPolicyProvider.INT_CONFIG_TYPE;
+ }
+
+ @Override
+ public String getDefaultConfigValue() {
+ return "1";
+ }
+
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public boolean isMultiplSupported() {
+ return false;
+ }
+
+ @Override
+ public String getId() {
+ return ID;
+ }
+
+}
diff --git a/server-spi/src/main/java/org/keycloak/policy/NotUsernamePasswordPolicyProvider.java b/server-spi/src/main/java/org/keycloak/policy/NotUsernamePasswordPolicyProvider.java
new file mode 100644
index 0000000..82fa7b6
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/policy/NotUsernamePasswordPolicyProvider.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.policy;
+
+import org.keycloak.models.KeycloakContext;
+import org.keycloak.models.UserModel;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class NotUsernamePasswordPolicyProvider implements PasswordPolicyProvider {
+
+ private static final String ERROR_MESSAGE = "invalidPasswordNotUsernameMessage";
+
+ private KeycloakContext context;
+
+ public NotUsernamePasswordPolicyProvider(KeycloakContext context) {
+ this.context = context;
+ }
+
+ @Override
+ public PolicyError validate(String username, String password) {
+ return username.equals(password) ? new PolicyError(ERROR_MESSAGE) : null;
+ }
+
+ @Override
+ public PolicyError validate(UserModel user, String password) {
+ return validate(user.getUsername(), password);
+ }
+
+ @Override
+ public Object parseConfig(String value) {
+ return null;
+ }
+
+ @Override
+ public void close() {
+ }
+
+}
diff --git a/server-spi/src/main/java/org/keycloak/policy/NotUsernamePasswordPolicyProviderFactory.java b/server-spi/src/main/java/org/keycloak/policy/NotUsernamePasswordPolicyProviderFactory.java
new file mode 100644
index 0000000..30ebbff
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/policy/NotUsernamePasswordPolicyProviderFactory.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.policy;
+
+import org.keycloak.Config;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class NotUsernamePasswordPolicyProviderFactory implements PasswordPolicyProviderFactory {
+
+ static final String ID = "notUsername";
+
+ @Override
+ public String getId() {
+ return ID;
+ }
+
+ @Override
+ public PasswordPolicyProvider create(KeycloakSession session) {
+ return new NotUsernamePasswordPolicyProvider(session.getContext());
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+ }
+
+ @Override
+ public String getDisplayName() {
+ return "Not Username";
+ }
+
+ @Override
+ public String getConfigType() {
+ return null;
+ }
+
+ @Override
+ public String getDefaultConfigValue() {
+ return null;
+ }
+
+ @Override
+ public boolean isMultiplSupported() {
+ return false;
+ }
+
+ @Override
+ public void close() {
+ }
+
+}
diff --git a/server-spi/src/main/java/org/keycloak/policy/PasswordPolicyManagerProvider.java b/server-spi/src/main/java/org/keycloak/policy/PasswordPolicyManagerProvider.java
new file mode 100644
index 0000000..3039c95
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/policy/PasswordPolicyManagerProvider.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.policy;
+
+import org.keycloak.models.UserModel;
+import org.keycloak.provider.Provider;
+
+/**
+ * @author <a href="mailto:roelof.naude@epiuse.com">Roelof Naude</a>
+ */
+public interface PasswordPolicyManagerProvider extends Provider {
+
+ PolicyError validate(UserModel user, String password);
+ PolicyError validate(String user, String password);
+
+}
diff --git a/server-spi/src/main/java/org/keycloak/policy/PasswordPolicyManagerProviderFactory.java b/server-spi/src/main/java/org/keycloak/policy/PasswordPolicyManagerProviderFactory.java
new file mode 100644
index 0000000..f68701e
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/policy/PasswordPolicyManagerProviderFactory.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.policy;
+
+import org.keycloak.provider.ProviderFactory;
+
+/**
+ * @author <a href="mailto:roelof.naude@epiuse.com">Roelof Naude</a>
+ */
+public interface PasswordPolicyManagerProviderFactory extends ProviderFactory<PasswordPolicyManagerProvider> {
+
+}
diff --git a/server-spi/src/main/java/org/keycloak/policy/PasswordPolicyManagerSpi.java b/server-spi/src/main/java/org/keycloak/policy/PasswordPolicyManagerSpi.java
new file mode 100644
index 0000000..266cf1f
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/policy/PasswordPolicyManagerSpi.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.policy;
+
+import org.keycloak.provider.Provider;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class PasswordPolicyManagerSpi implements Spi {
+
+ @Override
+ public boolean isInternal() {
+ return true;
+ }
+
+ @Override
+ public String getName() {
+ return "password-policy-manager";
+ }
+
+ @Override
+ public Class<? extends Provider> getProviderClass() {
+ return PasswordPolicyManagerProvider.class;
+ }
+
+ @Override
+ public Class<? extends ProviderFactory> getProviderFactoryClass() {
+ return PasswordPolicyManagerProviderFactory.class;
+ }
+
+}
diff --git a/server-spi/src/main/java/org/keycloak/policy/PasswordPolicyProvider.java b/server-spi/src/main/java/org/keycloak/policy/PasswordPolicyProvider.java
new file mode 100644
index 0000000..96b1803
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/policy/PasswordPolicyProvider.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.policy;
+
+import org.keycloak.models.UserModel;
+import org.keycloak.provider.Provider;
+
+/**
+ * @author <a href="mailto:roelof.naude@epiuse.com">Roelof Naude</a>
+ */
+public interface PasswordPolicyProvider extends Provider {
+
+ String STRING_CONFIG_TYPE = "String";
+ String INT_CONFIG_TYPE = "int";
+
+ PolicyError validate(UserModel user, String password);
+ PolicyError validate(String user, String password);
+ Object parseConfig(String value);
+
+}
diff --git a/server-spi/src/main/java/org/keycloak/policy/PasswordPolicyProviderFactory.java b/server-spi/src/main/java/org/keycloak/policy/PasswordPolicyProviderFactory.java
new file mode 100644
index 0000000..44714e3
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/policy/PasswordPolicyProviderFactory.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.policy;
+
+import org.keycloak.provider.ProviderFactory;
+
+/**
+ * @author <a href="mailto:roelof.naude@epiuse.com">Roelof Naude</a>
+ */
+public interface PasswordPolicyProviderFactory extends ProviderFactory<PasswordPolicyProvider> {
+
+ String getDisplayName();
+ String getConfigType();
+ String getDefaultConfigValue();
+ boolean isMultiplSupported();
+
+}
diff --git a/server-spi/src/main/java/org/keycloak/policy/PasswordPolicySpi.java b/server-spi/src/main/java/org/keycloak/policy/PasswordPolicySpi.java
new file mode 100644
index 0000000..97ad19a
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/policy/PasswordPolicySpi.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.policy;
+
+import org.keycloak.provider.Provider;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+
+/**
+ * @author <a href="mailto:roelof.naude@epiuse.com">Roelof Naude</a>
+ */
+public class PasswordPolicySpi implements Spi {
+
+ @Override
+ public boolean isInternal() {
+ return false;
+ }
+
+ @Override
+ public String getName() {
+ return "password-policy";
+ }
+
+ @Override
+ public Class<? extends Provider> getProviderClass() {
+ return PasswordPolicyProvider.class;
+ }
+
+ @Override
+ public Class<? extends ProviderFactory> getProviderFactoryClass() {
+ return PasswordPolicyProviderFactory.class;
+ }
+}
diff --git a/server-spi/src/main/java/org/keycloak/policy/PolicyError.java b/server-spi/src/main/java/org/keycloak/policy/PolicyError.java
new file mode 100644
index 0000000..d3a84f0
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/policy/PolicyError.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.policy;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public final class PolicyError {
+ private String message;
+ private Object[] parameters;
+
+ public PolicyError(String message, Object... parameters) {
+ this.message = message;
+ this.parameters = parameters;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public Object[] getParameters() {
+ return parameters;
+ }
+}
diff --git a/server-spi/src/main/java/org/keycloak/policy/RegexPatternsPasswordPolicyProvider.java b/server-spi/src/main/java/org/keycloak/policy/RegexPatternsPasswordPolicyProvider.java
new file mode 100644
index 0000000..7d4dbcc
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/policy/RegexPatternsPasswordPolicyProvider.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.policy;
+
+import org.keycloak.models.KeycloakContext;
+import org.keycloak.models.UserModel;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class RegexPatternsPasswordPolicyProvider implements PasswordPolicyProvider {
+
+ private static final String ERROR_MESSAGE = "invalidPasswordRegexPatternMessage";
+
+ private KeycloakContext context;
+
+ public RegexPatternsPasswordPolicyProvider(KeycloakContext context) {
+ this.context = context;
+ }
+
+ @Override
+ public PolicyError validate(String username, String password) {
+ Pattern pattern = context.getRealm().getPasswordPolicy().getPolicyConfig(RegexPatternsPasswordPolicyProviderFactory.ID);
+ Matcher matcher = pattern.matcher(password);
+ if (!matcher.matches()) {
+ return new PolicyError(ERROR_MESSAGE, pattern.pattern());
+ }
+ return null;
+ }
+
+ @Override
+ public PolicyError validate(UserModel user, String password) {
+ return validate(user.getUsername(), password);
+ }
+
+ @Override
+ public Object parseConfig(String value) {
+ if (value == null) {
+ throw new IllegalArgumentException("Config required");
+ }
+ return Pattern.compile(value);
+ }
+
+ @Override
+ public void close() {
+ }
+
+}
diff --git a/server-spi/src/main/java/org/keycloak/policy/RegexPatternsPasswordPolicyProviderFactory.java b/server-spi/src/main/java/org/keycloak/policy/RegexPatternsPasswordPolicyProviderFactory.java
new file mode 100644
index 0000000..c0ce732
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/policy/RegexPatternsPasswordPolicyProviderFactory.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.policy;
+
+import org.keycloak.Config;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class RegexPatternsPasswordPolicyProviderFactory implements PasswordPolicyProviderFactory {
+
+ static final String ID = "regexPattern";
+
+ @Override
+ public String getId() {
+ return ID;
+ }
+
+ @Override
+ public PasswordPolicyProvider create(KeycloakSession session) {
+ return new RegexPatternsPasswordPolicyProvider(session.getContext());
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+ }
+
+ @Override
+ public String getDisplayName() {
+ return "Regular Expression";
+ }
+
+ @Override
+ public String getConfigType() {
+ return PasswordPolicyProvider.STRING_CONFIG_TYPE;
+ }
+
+ @Override
+ public String getDefaultConfigValue() {
+ return "";
+ }
+
+ @Override
+ public boolean isMultiplSupported() {
+ return true;
+ }
+
+ @Override
+ public void close() {
+ }
+
+}
diff --git a/server-spi/src/main/java/org/keycloak/policy/SpecialCharsPasswordPolicyProvider.java b/server-spi/src/main/java/org/keycloak/policy/SpecialCharsPasswordPolicyProvider.java
new file mode 100644
index 0000000..694d81e
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/policy/SpecialCharsPasswordPolicyProvider.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.policy;
+
+import org.keycloak.models.KeycloakContext;
+import org.keycloak.models.UserModel;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class SpecialCharsPasswordPolicyProvider implements PasswordPolicyProvider {
+
+ private static final String ERROR_MESSAGE = "invalidPasswordMinSpecialCharsMessage";
+
+ private KeycloakContext context;
+
+ public SpecialCharsPasswordPolicyProvider(KeycloakContext context) {
+ this.context = context;
+ }
+
+ @Override
+ public PolicyError validate(String username, String password) {
+ int min = context.getRealm().getPasswordPolicy().getPolicyConfig(SpecialCharsPasswordPolicyProviderFactory.ID);
+ int count = 0;
+ for (char c : password.toCharArray()) {
+ if (!Character.isLetterOrDigit(c)) {
+ count++;
+ }
+ }
+ return count < min ? new PolicyError(ERROR_MESSAGE, min) : null;
+ }
+
+ @Override
+ public PolicyError validate(UserModel user, String password) {
+ return validate(user.getUsername(), password);
+ }
+
+ @Override
+ public Object parseConfig(String value) {
+ return value != null ? Integer.parseInt(value) : 1;
+ }
+
+ @Override
+ public void close() {
+ }
+
+}
diff --git a/server-spi/src/main/java/org/keycloak/policy/SpecialCharsPasswordPolicyProviderFactory.java b/server-spi/src/main/java/org/keycloak/policy/SpecialCharsPasswordPolicyProviderFactory.java
new file mode 100644
index 0000000..908cbee
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/policy/SpecialCharsPasswordPolicyProviderFactory.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.policy;
+
+import org.keycloak.Config;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class SpecialCharsPasswordPolicyProviderFactory implements PasswordPolicyProviderFactory {
+
+ public static final String ID = "specialChars";
+
+ @Override
+ public PasswordPolicyProvider create(KeycloakSession session) {
+ return new SpecialCharsPasswordPolicyProvider(session.getContext());
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+ }
+
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public String getDisplayName() {
+ return "Special Characters";
+ }
+
+ @Override
+ public String getConfigType() {
+ return PasswordPolicyProvider.INT_CONFIG_TYPE;
+ }
+
+ @Override
+ public String getDefaultConfigValue() {
+ return "1";
+ }
+
+ @Override
+ public boolean isMultiplSupported() {
+ return false;
+ }
+
+ @Override
+ public String getId() {
+ return ID;
+ }
+
+}
diff --git a/server-spi/src/main/java/org/keycloak/policy/UpperCasePasswordPolicyProvider.java b/server-spi/src/main/java/org/keycloak/policy/UpperCasePasswordPolicyProvider.java
new file mode 100644
index 0000000..d8a570b
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/policy/UpperCasePasswordPolicyProvider.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.policy;
+
+import org.keycloak.models.KeycloakContext;
+import org.keycloak.models.UserModel;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class UpperCasePasswordPolicyProvider implements PasswordPolicyProvider {
+
+ private static final String ERROR_MESSAGE = "invalidPasswordMinUpperCaseCharsMessage";
+
+ private KeycloakContext context;
+
+ public UpperCasePasswordPolicyProvider(KeycloakContext context) {
+ this.context = context;
+ }
+
+ @Override
+ public PolicyError validate(String username, String password) {
+ int min = context.getRealm().getPasswordPolicy().getPolicyConfig(UpperCasePasswordPolicyProviderFactory.ID);
+ int count = 0;
+ for (char c : password.toCharArray()) {
+ if (Character.isUpperCase(c)) {
+ count++;
+ }
+ }
+ return count < min ? new PolicyError(ERROR_MESSAGE, min) : null;
+ }
+
+ @Override
+ public PolicyError validate(UserModel user, String password) {
+ return validate(user.getUsername(), password);
+ }
+
+ @Override
+ public Object parseConfig(String value) {
+ return value != null ? Integer.parseInt(value) : 1;
+ }
+
+ @Override
+ public void close() {
+ }
+
+}
diff --git a/server-spi/src/main/java/org/keycloak/policy/UpperCasePasswordPolicyProviderFactory.java b/server-spi/src/main/java/org/keycloak/policy/UpperCasePasswordPolicyProviderFactory.java
new file mode 100644
index 0000000..8dce247
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/policy/UpperCasePasswordPolicyProviderFactory.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.policy;
+
+import org.keycloak.Config;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class UpperCasePasswordPolicyProviderFactory implements PasswordPolicyProviderFactory {
+
+ public static final String ID = "upperCase";
+
+ @Override
+ public PasswordPolicyProvider create(KeycloakSession session) {
+ return new UpperCasePasswordPolicyProvider(session.getContext());
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+ }
+
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public String getDisplayName() {
+ return "Uppercase Characters";
+ }
+
+ @Override
+ public String getConfigType() {
+ return PasswordPolicyProvider.INT_CONFIG_TYPE;
+ }
+
+ @Override
+ public String getDefaultConfigValue() {
+ return "1";
+ }
+
+ @Override
+ public boolean isMultiplSupported() {
+ return false;
+ }
+
+ @Override
+ public String getId() {
+ return ID;
+ }
+
+}
diff --git a/server-spi/src/main/java/org/keycloak/provider/ProviderConfigProperty.java b/server-spi/src/main/java/org/keycloak/provider/ProviderConfigProperty.java
index 93f7aad..5ddfb4c 100755
--- a/server-spi/src/main/java/org/keycloak/provider/ProviderConfigProperty.java
+++ b/server-spi/src/main/java/org/keycloak/provider/ProviderConfigProperty.java
@@ -36,6 +36,17 @@ public class ProviderConfigProperty {
protected String type;
protected Object defaultValue;
+ public ProviderConfigProperty() {
+ }
+
+ public ProviderConfigProperty(String name, String label, String helpText, String type, Object defaultValue) {
+ this.name = name;
+ this.label = label;
+ this.helpText = helpText;
+ this.type = type;
+ this.defaultValue = defaultValue;
+ }
+
public String getName() {
return name;
}
diff --git a/server-spi/src/main/resources/META-INF/services/org.keycloak.policy.PasswordPolicyManagerProviderFactory b/server-spi/src/main/resources/META-INF/services/org.keycloak.policy.PasswordPolicyManagerProviderFactory
new file mode 100644
index 0000000..128272d
--- /dev/null
+++ b/server-spi/src/main/resources/META-INF/services/org.keycloak.policy.PasswordPolicyManagerProviderFactory
@@ -0,0 +1,18 @@
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+org.keycloak.policy.DefaultPasswordPolicyManagerProviderFactory
\ No newline at end of file
diff --git a/server-spi/src/main/resources/META-INF/services/org.keycloak.policy.PasswordPolicyProviderFactory b/server-spi/src/main/resources/META-INF/services/org.keycloak.policy.PasswordPolicyProviderFactory
new file mode 100644
index 0000000..a436fe9
--- /dev/null
+++ b/server-spi/src/main/resources/META-INF/services/org.keycloak.policy.PasswordPolicyProviderFactory
@@ -0,0 +1,28 @@
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+org.keycloak.policy.DigitsPasswordPolicyProviderFactory
+org.keycloak.policy.ForceExpiredPasswordPolicyProviderFactory
+org.keycloak.policy.HashAlgorithmPasswordPolicyProviderFactory
+org.keycloak.policy.HashIterationsPasswordPolicyProviderFactory
+org.keycloak.policy.HistoryPasswordPolicyProviderFactory
+org.keycloak.policy.LengthPasswordPolicyProviderFactory
+org.keycloak.policy.LowerCasePasswordPolicyProviderFactory
+org.keycloak.policy.NotUsernamePasswordPolicyProviderFactory
+org.keycloak.policy.RegexPatternsPasswordPolicyProviderFactory
+org.keycloak.policy.SpecialCharsPasswordPolicyProviderFactory
+org.keycloak.policy.UpperCasePasswordPolicyProviderFactory
diff --git a/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi
index c5a5ebb..696bb4d 100755
--- a/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi
+++ b/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi
@@ -60,4 +60,5 @@ org.keycloak.authorization.store.StoreFactorySpi
org.keycloak.authorization.AuthorizationSpi
org.keycloak.models.cache.authorization.CachedStoreFactorySpi
org.keycloak.protocol.oidc.TokenIntrospectionSpi
-
+org.keycloak.policy.PasswordPolicySpi
+org.keycloak.policy.PasswordPolicyManagerSpi
\ No newline at end of file
diff --git a/services/src/main/java/org/keycloak/authentication/forms/RegistrationPassword.java b/services/src/main/java/org/keycloak/authentication/forms/RegistrationPassword.java
index 7f2b0c8..a42a073 100755
--- a/services/src/main/java/org/keycloak/authentication/forms/RegistrationPassword.java
+++ b/services/src/main/java/org/keycloak/authentication/forms/RegistrationPassword.java
@@ -33,6 +33,8 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.FormMessage;
+import org.keycloak.policy.PasswordPolicyManagerProvider;
+import org.keycloak.policy.PolicyError;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.messages.Messages;
@@ -70,7 +72,7 @@ public class RegistrationPassword implements FormAction, FormActionFactory {
errors.add(new FormMessage(RegistrationPage.FIELD_PASSWORD_CONFIRM, Messages.INVALID_PASSWORD_CONFIRM));
}
if (formData.getFirst(RegistrationPage.FIELD_PASSWORD) != null) {
- PasswordPolicy.Error err = context.getRealm().getPasswordPolicy().validate(context.getSession(), context.getRealm().isRegistrationEmailAsUsername() ? formData.getFirst(RegistrationPage.FIELD_EMAIL) : formData.getFirst(RegistrationPage.FIELD_USERNAME), formData.getFirst(RegistrationPage.FIELD_PASSWORD));
+ PolicyError err = context.getSession().getProvider(PasswordPolicyManagerProvider.class).validate(context.getRealm().isRegistrationEmailAsUsername() ? formData.getFirst(RegistrationPage.FIELD_EMAIL) : formData.getFirst(RegistrationPage.FIELD_USERNAME), formData.getFirst(RegistrationPage.FIELD_PASSWORD));
if (err != null)
errors.add(new FormMessage(RegistrationPage.FIELD_PASSWORD, err.getMessage(), err.getParameters()));
}
diff --git a/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java b/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java
index 99276d0..356922e 100755
--- a/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java
+++ b/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java
@@ -84,6 +84,8 @@ public class ApplianceBootstrap {
public void createMasterRealmUser(String username, String password) {
RealmModel realm = session.realms().getRealm(Config.getAdminRealm());
+ session.getContext().setRealm(realm);
+
if (session.users().getUsersCount(realm) > 0) {
throw new IllegalStateException("Can't create initial user as users already exists");
}
diff --git a/services/src/main/java/org/keycloak/services/managers/RealmManager.java b/services/src/main/java/org/keycloak/services/managers/RealmManager.java
index b38e245..214abb3 100755
--- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java
@@ -220,7 +220,7 @@ public class RealmManager implements RealmImporter {
realm.setEventsListeners(Collections.singleton("jboss-logging"));
- realm.setPasswordPolicy(new PasswordPolicy("hashIterations(20000)"));
+ realm.setPasswordPolicy(PasswordPolicy.parse(session, "hashIterations(20000)"));
}
public boolean removeRealm(RealmModel realm) {
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java
index 3ea8fd1..1a67fb0 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java
@@ -35,6 +35,11 @@ import org.keycloak.broker.provider.IdentityProvider;
import org.keycloak.broker.provider.IdentityProviderFactory;
import org.keycloak.events.EventType;
import org.keycloak.events.admin.OperationType;
+import org.keycloak.models.PasswordPolicy;
+import org.keycloak.policy.PasswordPolicyProvider;
+import org.keycloak.policy.PasswordPolicyProviderFactory;
+import org.keycloak.provider.*;
+import org.keycloak.representations.idm.PasswordPolicyTypeRepresentation;
import org.keycloak.theme.Theme;
import org.keycloak.theme.ThemeProvider;
import org.keycloak.models.KeycloakSession;
@@ -44,10 +49,6 @@ import org.keycloak.protocol.ClientInstallationProvider;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.LoginProtocolFactory;
import org.keycloak.protocol.ProtocolMapper;
-import org.keycloak.provider.ProviderConfigProperty;
-import org.keycloak.provider.ProviderFactory;
-import org.keycloak.provider.ServerInfoAwareProviderFactory;
-import org.keycloak.provider.Spi;
import org.keycloak.representations.idm.ConfigPropertyRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.idm.ProtocolMapperTypeRepresentation;
@@ -88,6 +89,7 @@ public class ServerInfoAdminResource {
setProtocolMapperTypes(info);
setBuiltinProtocolMappers(info);
setClientInstallations(info);
+ setPasswordPolicies(info);
info.setEnums(ENUMS);
return info;
}
@@ -248,6 +250,20 @@ public class ServerInfoAdminResource {
}
}
+ private void setPasswordPolicies(ServerInfoRepresentation info) {
+ info.setPasswordPolicies(new LinkedList<>());
+ for (ProviderFactory f : session.getKeycloakSessionFactory().getProviderFactories(PasswordPolicyProvider.class)) {
+ PasswordPolicyProviderFactory factory = (PasswordPolicyProviderFactory) f;
+ PasswordPolicyTypeRepresentation rep = new PasswordPolicyTypeRepresentation();
+ rep.setId(factory.getId());
+ rep.setDisplayName(factory.getDisplayName());
+ rep.setConfigType(factory.getConfigType());
+ rep.setDefaultValue(factory.getDefaultConfigValue());
+ rep.setMultipleSupported(factory.isMultiplSupported());
+ info.getPasswordPolicies().add(rep);
+ }
+ }
+
private static Map<String, List<String>> createEnumsMap(Class... enums) {
Map<String, List<String>> m = new HashMap<>();
for (Class e : enums) {
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
index 1eee46b..4594bdd 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
@@ -280,7 +280,7 @@ public class RealmAdminResource {
}
}
- RepresentationToModel.updateRealm(rep, realm);
+ RepresentationToModel.updateRealm(rep, realm, session);
// Refresh periodic sync tasks for configured federationProviders
List<UserFederationProviderModel> federationProviders = realm.getUserFederationProviders();
diff --git a/services/src/main/java/org/keycloak/services/validation/Validation.java b/services/src/main/java/org/keycloak/services/validation/Validation.java
index b5a39bb..eacfa8f 100755
--- a/services/src/main/java/org/keycloak/services/validation/Validation.java
+++ b/services/src/main/java/org/keycloak/services/validation/Validation.java
@@ -22,6 +22,8 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.FormMessage;
+import org.keycloak.policy.PasswordPolicyManagerProvider;
+import org.keycloak.policy.PolicyError;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.messages.Messages;
@@ -73,7 +75,7 @@ public class Validation {
}
if (formData.getFirst(FIELD_PASSWORD) != null) {
- PasswordPolicy.Error err = policy.validate(session, realm.isRegistrationEmailAsUsername()?formData.getFirst(FIELD_EMAIL):formData.getFirst(FIELD_USERNAME), formData.getFirst(FIELD_PASSWORD));
+ PolicyError err = session.getProvider(PasswordPolicyManagerProvider.class).validate(realm.isRegistrationEmailAsUsername() ? formData.getFirst(FIELD_EMAIL) : formData.getFirst(FIELD_USERNAME), formData.getFirst(FIELD_PASSWORD));
if (err != null)
errors.add(new FormMessage(FIELD_PASSWORD, err.getMessage(), err.getParameters()));
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java
index f340379..a28af56 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java
@@ -164,11 +164,11 @@ public class AdapterTest extends AbstractModelTest {
Assert.assertTrue(userProvider.validCredentials(session, realmModel, user, UserCredentialModel.password("geheim")));
List<UserCredentialValueModel> creds = user.getCredentialsDirectly();
Assert.assertEquals(creds.get(0).getHashIterations(), 20000);
- realmModel.setPasswordPolicy(new PasswordPolicy("hashIterations(200)"));
+ realmModel.setPasswordPolicy(PasswordPolicy.parse(realmManager.getSession(), "hashIterations(200)"));
Assert.assertTrue(userProvider.validCredentials(session, realmModel, user, UserCredentialModel.password("geheim")));
creds = user.getCredentialsDirectly();
Assert.assertEquals(creds.get(0).getHashIterations(), 200);
- realmModel.setPasswordPolicy(new PasswordPolicy("hashIterations(1)"));
+ realmModel.setPasswordPolicy(PasswordPolicy.parse(realmManager.getSession(), "hashIterations(1)"));
}
@Test
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ModelTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ModelTest.java
index c356eb5..a34f460 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ModelTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ModelTest.java
@@ -42,7 +42,7 @@ public class ModelTest extends AbstractModelTest {
realm.setSslRequired(SslRequired.EXTERNAL);
realm.setVerifyEmail(true);
realm.setAccessTokenLifespan(1000);
- realm.setPasswordPolicy(new PasswordPolicy("length"));
+ realm.setPasswordPolicy(PasswordPolicy.parse(realmManager.getSession(), "length"));
realm.setAccessCodeLifespan(1001);
realm.setAccessCodeLifespanUserAction(1002);
KeycloakModelUtils.generateRealmKeys(realm);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/PasswordPolicyTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/PasswordPolicyTest.java
new file mode 100755
index 0000000..e21fb0b
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/PasswordPolicyTest.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.model;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.models.PasswordPolicy;
+import org.keycloak.models.RealmModel;
+import org.keycloak.policy.PasswordPolicyManagerProvider;
+
+import java.util.regex.PatternSyntaxException;
+
+import static org.junit.Assert.fail;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class PasswordPolicyTest extends AbstractModelTest {
+
+ private RealmModel realmModel;
+ private PasswordPolicyManagerProvider policyManager;
+
+ @Before
+ public void before() throws Exception {
+ super.before();
+ realmModel = realmManager.createRealm("JUGGLER");
+ session.getContext().setRealm(realmModel);
+ policyManager = session.getProvider(PasswordPolicyManagerProvider.class);
+ }
+
+ @Test
+ public void testLength() {
+ realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "length"));
+
+ Assert.assertEquals("invalidPasswordMinLengthMessage", policyManager.validate("jdoe", "1234567").getMessage());
+ Assert.assertArrayEquals(new Object[]{8}, policyManager.validate("jdoe", "1234567").getParameters());
+ Assert.assertNull(policyManager.validate("jdoe", "12345678"));
+
+ realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "length(4)"));
+
+ Assert.assertEquals("invalidPasswordMinLengthMessage", policyManager.validate("jdoe", "123").getMessage());
+ Assert.assertArrayEquals(new Object[]{4}, policyManager.validate("jdoe", "123").getParameters());
+ Assert.assertNull(policyManager.validate("jdoe", "1234"));
+ }
+
+ @Test
+ public void testDigits() {
+ realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "digits"));
+ Assert.assertEquals("invalidPasswordMinDigitsMessage", policyManager.validate("jdoe", "abcd").getMessage());
+ Assert.assertArrayEquals(new Object[]{1}, policyManager.validate("jdoe", "abcd").getParameters());
+ Assert.assertNull(policyManager.validate("jdoe", "abcd1"));
+
+ realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "digits(2)"));
+ Assert.assertEquals("invalidPasswordMinDigitsMessage", policyManager.validate("jdoe", "abcd1").getMessage());
+ Assert.assertArrayEquals(new Object[]{2}, policyManager.validate("jdoe", "abcd1").getParameters());
+ Assert.assertNull(policyManager.validate("jdoe", "abcd12"));
+ }
+
+ @Test
+ public void testLowerCase() {
+ realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "lowerCase"));
+ Assert.assertEquals("invalidPasswordMinLowerCaseCharsMessage", policyManager.validate("jdoe", "ABCD1234").getMessage());
+ Assert.assertArrayEquals(new Object[]{1}, policyManager.validate("jdoe", "ABCD1234").getParameters());
+ Assert.assertNull(policyManager.validate("jdoe", "ABcD1234"));
+
+ realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "lowerCase(2)"));
+ Assert.assertEquals("invalidPasswordMinLowerCaseCharsMessage", policyManager.validate("jdoe", "ABcD1234").getMessage());
+ Assert.assertArrayEquals(new Object[]{2}, policyManager.validate("jdoe", "ABcD1234").getParameters());
+ Assert.assertNull(policyManager.validate("jdoe", "aBcD1234"));
+ }
+
+ @Test
+ public void testUpperCase() {
+ realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "upperCase"));
+ Assert.assertEquals("invalidPasswordMinUpperCaseCharsMessage", policyManager.validate("jdoe", "abcd1234").getMessage());
+ Assert.assertArrayEquals(new Object[]{1}, policyManager.validate("jdoe", "abcd1234").getParameters());
+ Assert.assertNull(policyManager.validate("jdoe", "abCd1234"));
+
+ realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "upperCase(2)"));
+ Assert.assertEquals("invalidPasswordMinUpperCaseCharsMessage", policyManager.validate("jdoe", "abCd1234").getMessage());
+ Assert.assertArrayEquals(new Object[]{2}, policyManager.validate("jdoe", "abCd1234").getParameters());
+ Assert.assertNull(policyManager.validate("jdoe", "AbCd1234"));
+ }
+
+ @Test
+ public void testSpecialChars() {
+ realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "specialChars"));
+ Assert.assertEquals("invalidPasswordMinSpecialCharsMessage", policyManager.validate("jdoe", "abcd1234").getMessage());
+ Assert.assertArrayEquals(new Object[]{1}, policyManager.validate("jdoe", "abcd1234").getParameters());
+ Assert.assertNull(policyManager.validate("jdoe", "ab&d1234"));
+
+ realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "specialChars(2)"));
+ Assert.assertEquals("invalidPasswordMinSpecialCharsMessage", policyManager.validate("jdoe", "ab&d1234").getMessage());
+ Assert.assertArrayEquals(new Object[]{2}, policyManager.validate("jdoe", "ab&d1234").getParameters());
+ Assert.assertNull(policyManager.validate("jdoe", "ab&d-234"));
+ }
+
+ @Test
+ public void testNotUsername() {
+ realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "notUsername"));
+ Assert.assertEquals("invalidPasswordNotUsernameMessage", policyManager.validate("jdoe", "jdoe").getMessage());
+ Assert.assertNull(policyManager.validate("jdoe", "ab&d1234"));
+ }
+
+ @Test
+ public void testInvalidPolicyName() {
+ try {
+ realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "noSuchPolicy"));
+ Assert.fail("Expected exception");
+ } catch (IllegalArgumentException e) {
+ }
+ }
+
+ @Test
+ public void testRegexPatterns() {
+ PasswordPolicy policy = null;
+ try {
+ realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "regexPattern"));
+ fail("Expected NullPointerException: Regex Pattern cannot be null.");
+ } catch (IllegalArgumentException e) {
+ // Expected NPE as regex pattern is null.
+ }
+
+ try {
+ realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "regexPattern(*)"));
+ fail("Expected PatternSyntaxException: Regex Pattern cannot be null.");
+ } catch (PatternSyntaxException e) {
+ // Expected PSE as regex pattern(or any of its token) is not quantifiable.
+ }
+
+ try {
+ realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "regexPattern(*,**)"));
+ fail("Expected PatternSyntaxException: Regex Pattern cannot be null.");
+ } catch (PatternSyntaxException e) {
+ // Expected PSE as regex pattern(or any of its token) is not quantifiable.
+ }
+
+ //Fails to match one of the regex pattern
+ realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "regexPattern(jdoe) and regexPattern(j*d)"));
+ Assert.assertEquals("invalidPasswordRegexPatternMessage", policyManager.validate("jdoe", "jdoe").getMessage());
+
+ ////Fails to match all of the regex patterns
+ realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "regexPattern(j*p) and regexPattern(j*d) and regexPattern(adoe)"));
+ Assert.assertEquals("invalidPasswordRegexPatternMessage", policyManager.validate("jdoe", "jdoe").getMessage());
+
+ realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "regexPattern([a-z][a-z][a-z][a-z][0-9])"));
+ Assert.assertEquals("invalidPasswordRegexPatternMessage", policyManager.validate("jdoe", "jdoe").getMessage());
+
+ realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "regexPattern(jdoe)"));
+ Assert.assertNull(policyManager.validate("jdoe", "jdoe"));
+
+ realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "regexPattern([a-z][a-z][a-z][a-z][0-9])"));
+ Assert.assertNull(policyManager.validate("jdoe", "jdoe0"));
+ }
+
+ @Test
+ public void testComplex() {
+ realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "length(8) and digits(2) and lowerCase(2) and upperCase(2) and specialChars(2) and notUsername()"));
+ Assert.assertNotNull(policyManager.validate("jdoe", "12aaBB&"));
+ Assert.assertNotNull(policyManager.validate("jdoe", "aaaaBB&-"));
+ Assert.assertNotNull(policyManager.validate("jdoe", "12AABB&-"));
+ Assert.assertNotNull(policyManager.validate("jdoe", "12aabb&-"));
+ Assert.assertNotNull(policyManager.validate("jdoe", "12aaBBcc"));
+ Assert.assertNotNull(policyManager.validate("12aaBB&-", "12aaBB&-"));
+
+ Assert.assertNull(policyManager.validate("jdoe", "12aaBB&-"));
+ }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java
index ec76062..6796d12 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java
@@ -351,8 +351,7 @@ public class ResourceOwnerPasswordCredentialsGrantTest extends AbstractKeycloakT
public void grantAccessTokenExpiredPassword() throws Exception {
RealmResource realmResource = adminClient.realm("test");
- RealmManager.realm(realmResource).passwordPolicy(
- new PasswordPolicy("forceExpiredPasswordChange(1)").toString());
+ RealmManager.realm(realmResource).passwordPolicy("forceExpiredPasswordChange(1)");
try {
setTimeOffset(60 * 60 * 48);
@@ -376,7 +375,7 @@ public class ResourceOwnerPasswordCredentialsGrantTest extends AbstractKeycloakT
.user((String) null)
.assertEvent();
} finally {
- RealmManager.realm(realmResource).passwordPolicy(new PasswordPolicy("").toString());
+ RealmManager.realm(realmResource).passwordPolicy("");
UserManager.realm(realmResource).username("test-user@localhost")
.removeRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD.toString());
}
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index 4e23f6c..af199e3 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -919,4 +919,4 @@ clear-events=Clear events
saved-types=Saved Types
clear-admin-events=Clear admin events
clear-changes=Clear changes
-error=Error
+error=Error
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/app.js b/themes/src/main/resources/theme/base/admin/resources/js/app.js
index 2591bbf..e5cd7c1 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/app.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/app.js
@@ -1639,6 +1639,9 @@ module.config([ '$routeProvider', function($routeProvider) {
resolve : {
realm : function(RealmLoader) {
return RealmLoader();
+ },
+ serverInfo : function(ServerInfoLoader) {
+ return ServerInfoLoader();
}
},
controller : 'RealmPasswordPolicyCtrl'
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
index 65f45b6..68a4049 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
@@ -429,68 +429,84 @@ module.controller('RealmCacheCtrl', function($scope, realm, RealmClearUserCache,
});
-module.controller('RealmPasswordPolicyCtrl', function($scope, Realm, realm, $http, $location, Dialog, Notifications, PasswordPolicy) {
- console.log('RealmPasswordPolicyCtrl');
+module.controller('RealmPasswordPolicyCtrl', function($scope, Realm, realm, $http, $location, $route, Dialog, Notifications, serverInfo) {
+ var parse = function(policyString) {
+ var policies = [];
+ if (!policyString || policyString.length == 0){
+ return policies;
+ }
- $scope.realm = realm;
+ var policyArray = policyString.split(" and ");
- var oldCopy = angular.copy($scope.realm);
+ for (var i = 0; i < policyArray.length; i ++){
+ var policyToken = policyArray[i];
+ var id;
+ var value;
+ if (policyToken.indexOf('(') == -1) {
+ id = policyToken.trim();
+ } else {
+ id = policyToken.substring(0, policyToken.indexOf('('));
+ value = policyToken.substring(policyToken.indexOf('(') + 1, policyToken.indexOf(')')).trim();
+ }
+
+ for (var j = 0; j < serverInfo.passwordPolicies.length; j++) {
+ if (serverInfo.passwordPolicies[j].id == id) {
+ var p = serverInfo.passwordPolicies[j];
+ p.value = value && value || p.defaultValue;
+ policies.push(p);
+ }
+ }
+ }
+ return policies;
+ };
- $scope.allPolicies = PasswordPolicy.allPolicies;
- $scope.policyMessages = PasswordPolicy.policyMessages;
+ var toString = function(policies) {
+ if (!policies || policies.length == 0) {
+ return "";
+ }
+ var policyString = "";
+ for (var i = 0; i < policies.length; i++) {
+ policyString += policies[i].id;
+ if (policies[i].value && policies[i].value != policies[i].defaultValue) {
+ policyString += '(' + policies[i].value + ')';
+ }
+ policyString += " and ";
+ }
+ policyString = policyString.substring(0, policyString.length - 5);
+ return policyString;
+ }
- $scope.policy = PasswordPolicy.parse(realm.passwordPolicy);
- var oldPolicy = angular.copy($scope.policy);
+ $scope.realm = realm;
+ $scope.serverInfo = serverInfo;
+ $scope.changed = false; $scope.policy = parse(realm.passwordPolicy);
$scope.addPolicy = function(policy){
+ policy.value = policy.defaultValue;
if (!$scope.policy) {
$scope.policy = [];
}
- if (policy.name === 'regexPattern') {
- for (var i in $scope.allPolicies) {
- var p = $scope.allPolicies[i];
- if (p.name === 'regexPattern') {
- $scope.allPolicies[i] = { name: 'regexPattern', value: '' };
- }
- }
- }
$scope.policy.push(policy);
+ $scope.changed = true;
}
$scope.removePolicy = function(index){
$scope.policy.splice(index, 1);
+ $scope.changed = true;
}
- $scope.changed = false;
-
- $scope.$watch('realm', function() {
- if (!angular.equals($scope.realm, oldCopy)) {
- $scope.changed = true;
- }
- }, true);
-
- $scope.$watch('policy', function(oldVal, newVal) {
- if (!angular.equals($scope.policy, oldPolicy)) {
- $scope.realm.passwordPolicy = PasswordPolicy.toString($scope.policy);
- $scope.changed = true;
- }
- }, true);
-
$scope.save = function() {
$scope.changed = false;
+ $scope.realm.passwordPolicy = toString($scope.policy);
+ console.debug($scope.realm.passwordPolicy);
Realm.update($scope.realm, function () {
$location.url("/realms/" + realm.realm + "/authentication/password-policy");
Notifications.success("Your changes have been saved to the realm.");
- oldCopy = angular.copy($scope.realm);
- oldPolicy = angular.copy($scope.policy);
});
};
$scope.reset = function() {
- $scope.realm = angular.copy(oldCopy);
- $scope.policy = angular.copy(oldPolicy);
- $scope.changed = false;
+ $route.reload();
};
});
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/services.js b/themes/src/main/resources/theme/base/admin/resources/js/services.js
index 6630c25..c49a8d5 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/services.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/services.js
@@ -1238,90 +1238,6 @@ module.factory('TimeUnit2', function() {
return t;
});
-
-module.factory('PasswordPolicy', function() {
- var p = {};
-
- p.policyMessages = {
- hashAlgorithm: "Default hashing algorithm. Default is 'pbkdf2'.",
- 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",
- regexPattern: "Block passwords that do not match the regex pattern (string type).",
- passwordHistory: "Block passwords that are equal to previous passwords. Default value is 3.",
- forceExpiredPasswordChange: "Force password change when password credential is expired. Default value is 365 days."
- }
-
- p.allPolicies = [
- { name: 'hashAlgorithm', value: 'pbkdf2' },
- { name: 'hashIterations', value: 1 },
- { name: 'length', value: 8 },
- { name: 'digits', value: 1 },
- { name: 'lowerCase', value: 1 },
- { name: 'upperCase', value: 1 },
- { name: 'specialChars', value: 1 },
- { name: 'notUsername', value: 1 },
- { name: 'regexPattern', value: ''},
- { name: 'passwordHistory', value: 3 },
- { name: 'forceExpiredPasswordChange', value: 365 }
- ];
-
- p.parse = function(policyString) {
- var policies = [];
- var re, policyEntry;
-
- if (!policyString || policyString.length == 0){
- return policies;
- }
-
- var policyArray = policyString.split(" and ");
-
- for (var i = 0; i < policyArray.length; i ++){
- var policyToken = policyArray[i];
-
- if(policyToken.indexOf('hashAlgorithm') === 0 || policyToken.indexOf('regexPattern') === 0) {
- re = /(\w+)\((.*)\)/;
- policyEntry = re.exec(policyToken);
- if (null !== policyEntry) {
- policies.push({ name: policyEntry[1], value: policyEntry[2] });
- }
- } else {
- re = /(\w+)\(*(\d*)\)*/;
- policyEntry = re.exec(policyToken);
- if (null !== policyEntry) {
- policies.push({ name: policyEntry[1], value: parseInt(policyEntry[2]) });
- }
- }
- }
- return policies;
- };
-
- p.toString = function(policies) {
- if (!policies || policies.length == 0) {
- return "";
- }
- var policyString = "";
-
- for (var i = 0; i < policies.length; i++) {
- policyString += policies[i].name;
- if ( policies[i].value ){
- policyString += '(' + policies[i].value + ')';
- }
- policyString += " and ";
- }
-
- policyString = policyString.substring(0, policyString.length - 5);
-
- return policyString;
- };
-
- return p;
-});
-
module.filter('removeSelectedPolicies', function() {
return function(policies, selectedPolicies) {
var result = [];
@@ -1329,7 +1245,7 @@ module.filter('removeSelectedPolicies', function() {
var policy = policies[i];
var policyAvailable = true;
for(var j in selectedPolicies) {
- if(policy.name === selectedPolicies[j].name && policy.name !== 'regexPattern') {
+ if(policy.id === selectedPolicies[j].id && !policy.multipleSupported) {
policyAvailable = false;
}
}
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/password-policy.html b/themes/src/main/resources/theme/base/admin/resources/partials/password-policy.html
index 926ee80..e929cad 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/password-policy.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/password-policy.html
@@ -7,12 +7,12 @@
<table class="table table-striped table-bordered">
<caption class="hidden">{{:: 'table-of-password-policies' | translate}}</caption>
<thead>
- <tr ng-show="(allPolicies|removeSelectedPolicies:policy).length > 0">
+ <tr ng-show="(serverInfo.passwordPolicies|removeSelectedPolicies:policy).length > 0">
<th colspan="5" class="kc-table-actions">
<div class="pull-right">
<div>
<select class="form-control" ng-model="selectedPolicy"
- ng-options="(p.name|capitalize) for p in (allPolicies|removeSelectedPolicies:policy)"
+ ng-options="policy as policy.displayName for policy in (serverInfo.passwordPolicies|removeSelectedPolicies:policy) track by policy.id"
data-ng-change="addPolicy(selectedPolicy); selectedPolicy = null">
<option value="" disabled selected>{{:: 'add-policy.placeholder' | translate}}</option>
</select>
@@ -28,10 +28,9 @@
</thead>
<tbody>
<tr ng-repeat="p in policy">
- <td>{{p.name|capitalize}}</td>
+ <td>{{p.displayName}}</td>
<td>
- <input class="form-control" ng-model="p.value" ng-show="p.name != 'notUsername' "
- placeholder="{{:: 'no-value-assigned.placeholder' | translate}}" min="1" required>
+ <input type="text" class="form-control" ng-model="p.value" ng-show="p.configType" data-ng-required="!p.configType && !p.defaultValue">
</td>
<td class="kc-action-cell" ng-click="removePolicy($index)">{{:: 'delete' | translate}}</td>
</tr>