keycloak-uncached
Changes
model/api/pom.xml 5(+5 -0)
Details
diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-credentials.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-credentials.html
index 3b3c940..8b28419 100755
--- a/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-credentials.html
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-credentials.html
@@ -44,6 +44,13 @@
<input id="oauth" type="text" ui-select2="userCredentialOptions" ng-model="realm.requiredOAuthClientCredentials" placeholder="Type a role and enter">
</div>
</div>
+ <div class="form-group">
+ <label for="policy">Password Policy <span class="required" data-ng-show="createRealm">*</span></label>
+
+ <div class="controls">
+ <input class="xlarge" type="text" id="policy" name="policy" data-ng-model="realm.passwordPolicy" autofocus required>
+ </div>
+ </div>
</fieldset>
<div class="form-actions">
<button type="submit" kc-save class="primary" data-ng-show="changed">Save
diff --git a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
index c668a70..33c56f6 100755
--- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
@@ -32,6 +32,7 @@ public class RealmRepresentation {
protected Set<String> requiredCredentials;
protected Set<String> requiredApplicationCredentials;
protected Set<String> requiredOAuthClientCredentials;
+ protected String passwordPolicy;
protected List<UserRepresentation> users;
protected List<UserRoleMappingRepresentation> roleMappings;
protected List<ScopeMappingRepresentation> scopeMappings;
@@ -199,6 +200,14 @@ public class RealmRepresentation {
this.requiredOAuthClientCredentials = requiredOAuthClientCredentials;
}
+ public String getPasswordPolicy() {
+ return passwordPolicy;
+ }
+
+ public void setPasswordPolicy(String passwordPolicy) {
+ this.passwordPolicy = passwordPolicy;
+ }
+
public Integer getAccessCodeLifespan() {
return accessCodeLifespan;
}
diff --git a/forms/src/main/java/org/keycloak/forms/MessageBean.java b/forms/src/main/java/org/keycloak/forms/MessageBean.java
index c1b16a4..c24e66a 100644
--- a/forms/src/main/java/org/keycloak/forms/MessageBean.java
+++ b/forms/src/main/java/org/keycloak/forms/MessageBean.java
@@ -23,6 +23,8 @@ package org.keycloak.forms;
import org.keycloak.services.resources.flows.FormFlows;
+import java.util.ResourceBundle;
+
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@@ -32,13 +34,12 @@ public class MessageBean {
private FormFlows.MessageType type;
- // Message is considered ERROR by default
- public MessageBean(String summary) {
- this(summary, FormFlows.MessageType.ERROR);
- }
-
- public MessageBean(String summary, FormFlows.MessageType type) {
- this.summary = summary;
+ public MessageBean(String summary, FormFlows.MessageType type, ResourceBundle rb) {
+ if (rb.containsKey(summary)) {
+ this.summary = rb.getString(summary);
+ } else {
+ this.summary = summary;
+ }
this.type = type;
}
diff --git a/forms/src/main/java/org/keycloak/service/FormServiceImpl.java b/forms/src/main/java/org/keycloak/service/FormServiceImpl.java
index 329dbf2..596d893 100755
--- a/forms/src/main/java/org/keycloak/service/FormServiceImpl.java
+++ b/forms/src/main/java/org/keycloak/service/FormServiceImpl.java
@@ -79,19 +79,18 @@ public class FormServiceImpl implements FormService {
}
public String process(String pageId, FormServiceDataBean dataBean){
-
Map<String, Object> attributes = new HashMap<String, Object>();
+ ResourceBundle rb = ResourceBundle.getBundle(BUNDLE);
+ attributes.put("rb", rb);
+
if (dataBean.getMessage() != null){
- attributes.put("message", new MessageBean(dataBean.getMessage(), dataBean.getMessageType()));
+ attributes.put("message", new MessageBean(dataBean.getMessage(), dataBean.getMessageType(), rb));
}
RealmBean realm = new RealmBean(dataBean.getRealm());
attributes.put("template", new TemplateBean(realm, dataBean.getContextPath()));
- ResourceBundle rb = ResourceBundle.getBundle(BUNDLE);
- attributes.put("rb", rb);
-
if (commandMap.containsKey(pageId)){
commandMap.get(pageId).exec(attributes, dataBean);
}
diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login.ftl
index 10c668a..c46227b 100644
--- a/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login.ftl
+++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login.ftl
@@ -40,7 +40,7 @@
<#if message?has_content && message.error>
<div class="feedback error bottom-left show">
<p>
- <strong id="loginError">${rb.getString(message.summary)}</strong><br/>${rb.getString('emailErrorInfo')}
+ <strong id="loginError">${message.summary}</strong><br/>${rb.getString('emailErrorInfo')}
</p>
</div>
</#if>
diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login-action.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login-action.ftl
index f3113dc..977f1cd 100644
--- a/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login-action.ftl
+++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login-action.ftl
@@ -20,7 +20,7 @@
<div class="feedback-aligner">
<#if message?has_content && message.warning>
<div class="feedback warning show">
- <p><strong>${rb.getString('actionWarningHeader')} ${rb.getString(message.summary)}</strong><br/>${rb.getString('actionFollow')}</p>
+ <p><strong>${rb.getString('actionWarningHeader')} ${message.summary}</strong><br/>${rb.getString('actionFollow')}</p>
</div>
</#if>
</div>
@@ -47,13 +47,13 @@
<#if message.error>
<div class="feedback error bottom-left show">
<p>
- <strong id="loginError">${rb.getString(message.summary)}</strong><br/>${rb.getString('emailErrorInfo')}
+ <strong id="loginError">${message.summary}</strong><br/>${rb.getString('emailErrorInfo')}
</p>
</div>
<#elseif message.success>
<div class="feedback success bottom-left show">
<p>
- <strong>${rb.getString('successHeader')}</strong> ${rb.getString(message.summary)}
+ <strong>${rb.getString('successHeader')}</strong> ${message.summary}
</p>
</div>
</#if>
diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/template-main.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/template-main.ftl
index 4a45d93..437ceb7 100644
--- a/forms/src/main/resources/META-INF/resources/forms/theme/default/template-main.ftl
+++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/template-main.ftl
@@ -31,10 +31,10 @@
<#if message?has_content>
<div class="feedback-aligner">
<#if message.success>
- <div class="feedback success show"><p><strong>${rb.getString('successHeader')}</strong> ${rb.getString(message.summary)}</p></div>
+ <div class="feedback success show"><p><strong>${rb.getString('successHeader')}</strong> ${message.summary}</p></div>
</#if>
<#if message.error>
- <div class="feedback error show"><p><strong>${rb.getString('errorHeader')}</strong> ${rb.getString(message.summary)}</p></div>
+ <div class="feedback error show"><p><strong>${rb.getString('errorHeader')}</strong> ${message.summary}</p></div>
</#if>
</div>
</#if>
model/api/pom.xml 5(+5 -0)
diff --git a/model/api/pom.xml b/model/api/pom.xml
index 48e4ac9..ee9aee5 100755
--- a/model/api/pom.xml
+++ b/model/api/pom.xml
@@ -13,6 +13,11 @@
<description/>
<dependencies>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
<plugins>
diff --git a/model/api/src/main/java/org/keycloak/models/PasswordPolicy.java b/model/api/src/main/java/org/keycloak/models/PasswordPolicy.java
new file mode 100644
index 0000000..99636b9
--- /dev/null
+++ b/model/api/src/main/java/org/keycloak/models/PasswordPolicy.java
@@ -0,0 +1,182 @@
+package org.keycloak.models;
+
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class PasswordPolicy {
+
+ private List<Policy> policies;
+ private String policyString;
+
+ public PasswordPolicy(String policyString) {
+ if (policyString == null || policyString.length() == 0) {
+ this.policyString = null;
+ policies = Collections.emptyList();
+ } else {
+ this.policyString = policyString;
+ policies = parse(policyString);
+ }
+ }
+
+ private static List<Policy> parse(String policyString) {
+ List<Policy> list = new LinkedList<Policy>();
+ String[] policies = policyString.split(" and ");
+ for (String policy : policies) {
+ policy = policy.trim();
+
+ String name;
+ String[] args = null;
+
+ int i = policy.indexOf('(');
+ if (i == -1) {
+ name = policy.trim();
+ } else {
+ name = policy.substring(0, i).trim();
+ args = policy.substring(i + 1, policy.length() - 1).split(",");
+ for (int j = 0; j < args.length; j++) {
+ args[j] = args[j].trim();
+ }
+ }
+
+ if (name.equals(Length.NAME)) {
+ list.add(new Length(args));
+ } else if (name.equals(Digits.NAME)) {
+ list.add(new Digits(args));
+ } else if (name.equals(LowerCase.NAME)) {
+ list.add(new LowerCase(args));
+ } else if (name.equals(UpperCase.NAME)) {
+ list.add(new UpperCase(args));
+ } else if (name.equals(SpecialChars.NAME)) {
+ list.add(new SpecialChars(args));
+ }
+ }
+ return list;
+ }
+
+ public String validate(String password) {
+ for (Policy p : policies) {
+ String error = p.validate(password);
+ if (error != null) {
+ return error;
+ }
+ }
+ return null;
+ }
+
+ private static interface Policy {
+ public String validate(String password);
+ }
+
+ private static class Length implements Policy {
+ private static final String NAME = "length";
+ private int min;
+
+ public Length(String[] args) {
+ min = intArg(NAME, 8, args);
+ }
+
+ @Override
+ public String validate(String password) {
+ return password.length() < min ? "Invalid password: minimum length " + min : null;
+ }
+ }
+
+ private static class Digits implements Policy {
+ private static final String NAME = "digits";
+ private int min;
+
+ public Digits(String[] args) {
+ min = intArg(NAME, 1, args);
+ }
+
+ @Override
+ public String validate(String password) {
+ int count = 0;
+ for (char c : password.toCharArray()) {
+ if (Character.isDigit(c)) {
+ count++;
+ }
+ }
+ return count < min ? "Invalid password: must contain at least " + count + " numerical digits" : null;
+ }
+ }
+
+ private static class LowerCase implements Policy {
+ private static final String NAME = "lowerCase";
+ private int min;
+
+ public LowerCase(String[] args) {
+ min = intArg(NAME, 1, args);
+ }
+
+ @Override
+ public String validate(String password) {
+ int count = 0;
+ for (char c : password.toCharArray()) {
+ if (Character.isLowerCase(c)) {
+ count++;
+ }
+ }
+ return count < min ? "Invalid password: must contain at least " + count + " lower case characters": null;
+ }
+ }
+
+ private static class UpperCase implements Policy {
+ private static final String NAME = "upperCase";
+ private int min;
+
+ public UpperCase(String[] args) {
+ min = intArg(NAME, 1, args);
+ }
+
+ @Override
+ public String validate(String password) {
+ int count = 0;
+ for (char c : password.toCharArray()) {
+ if (Character.isUpperCase(c)) {
+ count++;
+ }
+ }
+ return count < min ? "Invalid password: must contain at least " + count + " upper case characters" : null;
+ }
+ }
+
+ private static class SpecialChars implements Policy {
+ private static final String NAME = "specialChars";
+ private int min;
+
+ public SpecialChars(String[] args) {
+ min = intArg(NAME, 1, args);
+ }
+
+ @Override
+ public String validate(String password) {
+ int count = 0;
+ for (char c : password.toCharArray()) {
+ if (!Character.isLetterOrDigit(c)) {
+ count++;
+ }
+ }
+ return count < min ? "Invalid password: must contain at least " + count + " special characters" : null;
+ }
+ }
+
+ private static int intArg(String policy, int defaultValue, String... args) {
+ if (args == null || args.length == 0) {
+ return defaultValue;
+ } else if (args.length == 1) {
+ return Integer.parseInt(args[0]);
+ } else {
+ throw new IllegalArgumentException("Invalid arguments to " + policy + ", expect no argument or single integer");
+ }
+ }
+
+ @Override
+ public String toString() {
+ return policyString;
+ }
+}
diff --git a/model/api/src/main/java/org/keycloak/models/RealmModel.java b/model/api/src/main/java/org/keycloak/models/RealmModel.java
index d4af49f..a4527ae 100755
--- a/model/api/src/main/java/org/keycloak/models/RealmModel.java
+++ b/model/api/src/main/java/org/keycloak/models/RealmModel.java
@@ -74,6 +74,10 @@ public interface RealmModel extends RoleContainerModel, RoleMapperModel, ScopeMa
void addRequiredCredential(String cred);
+ PasswordPolicy getPasswordPolicy();
+
+ void setPasswordPolicy(PasswordPolicy policy);
+
boolean validatePassword(UserModel user, String password);
boolean validateTOTP(UserModel user, String password, String token);
diff --git a/model/api/src/main/test/java/org/keycloak/models/PasswordPolicyTest.java b/model/api/src/main/test/java/org/keycloak/models/PasswordPolicyTest.java
new file mode 100644
index 0000000..3fd7271
--- /dev/null
+++ b/model/api/src/main/test/java/org/keycloak/models/PasswordPolicyTest.java
@@ -0,0 +1,78 @@
+package org.keycloak.models;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class PasswordPolicyTest {
+
+ @Test
+ public void testLength() {
+ PasswordPolicy policy = new PasswordPolicy("length");
+ Assert.assertNotNull(policy.validate("1234567"));
+ Assert.assertNull(policy.validate("12345678"));
+
+ policy = new PasswordPolicy("length(4)");
+ Assert.assertNotNull(policy.validate("123"));
+ Assert.assertNull(policy.validate("1234"));
+ }
+
+ @Test
+ public void testDigits() {
+ PasswordPolicy policy = new PasswordPolicy("digits");
+ Assert.assertNotNull(policy.validate("abcd"));
+ Assert.assertNull(policy.validate("abcd1"));
+
+ policy = new PasswordPolicy("digits(2)");
+ Assert.assertNotNull(policy.validate("abcd1"));
+ Assert.assertNull(policy.validate("abcd12"));
+ }
+
+ @Test
+ public void testLowerCase() {
+ PasswordPolicy policy = new PasswordPolicy("lowerCase");
+ Assert.assertNotNull(policy.validate("ABCD1234"));
+ Assert.assertNull(policy.validate("ABcD1234"));
+
+ policy = new PasswordPolicy("lowerCase(2)");
+ Assert.assertNotNull(policy.validate("ABcD1234"));
+ Assert.assertNull(policy.validate("aBcD1234"));
+ }
+
+ @Test
+ public void testUpperCase() {
+ PasswordPolicy policy = new PasswordPolicy("upperCase");
+ Assert.assertNotNull(policy.validate("abcd1234"));
+ Assert.assertNull(policy.validate("abCd1234"));
+
+ policy = new PasswordPolicy("upperCase(2)");
+ Assert.assertNotNull(policy.validate("abCd1234"));
+ Assert.assertNull(policy.validate("AbCd1234"));
+ }
+
+ @Test
+ public void testSpecialChars() {
+ PasswordPolicy policy = new PasswordPolicy("specialChars");
+ Assert.assertNotNull(policy.validate("abcd1234"));
+ Assert.assertNull(policy.validate("ab&d1234"));
+
+ policy = new PasswordPolicy("specialChars(2)");
+ Assert.assertNotNull(policy.validate("ab&d1234"));
+ Assert.assertNull(policy.validate("ab&d-234"));
+ }
+
+ @Test
+ public void testComplex() {
+ PasswordPolicy policy = new PasswordPolicy("length(8) and digits(2) and lowerCase(2) and upperCase(2) and specialChars(2)");
+ Assert.assertNotNull(policy.validate("12aaBB&"));
+ Assert.assertNotNull(policy.validate("aaaaBB&-"));
+ Assert.assertNotNull(policy.validate("12AABB&-"));
+ Assert.assertNotNull(policy.validate("12aabb&-"));
+ Assert.assertNotNull(policy.validate("12aaBBcc"));
+
+ Assert.assertNull(policy.validate("12aaBB&-"));
+ }
+
+}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
index d82c68e..5a59a4c 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
@@ -34,6 +34,7 @@ public class RealmEntity {
protected boolean resetPasswordAllowed;
protected boolean social;
protected boolean automaticRegistrationAfterSocialLogin;
+ protected String passwordPolicy;
protected int tokenLifespan;
protected int accessCodeLifespan;
@@ -269,4 +270,13 @@ public class RealmEntity {
public void setDefaultRoles(Collection<RoleEntity> defaultRoles) {
this.defaultRoles = defaultRoles;
}
+
+ public String getPasswordPolicy() {
+ return passwordPolicy;
+ }
+
+ public void setPasswordPolicy(String passwordPolicy) {
+ this.passwordPolicy = passwordPolicy;
+ }
}
+
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 7a47448..6e6c8f1 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
@@ -4,6 +4,7 @@ import org.bouncycastle.openssl.PEMWriter;
import org.keycloak.PemUtils;
import org.keycloak.models.ApplicationModel;
import org.keycloak.models.OAuthClientModel;
+import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredCredentialModel;
import org.keycloak.models.RoleModel;
@@ -37,6 +38,7 @@ public class RealmAdapter implements RealmModel {
protected EntityManager em;
protected volatile transient PublicKey publicKey;
protected volatile transient PrivateKey privateKey;
+ private PasswordPolicy passwordPolicy;
public RealmAdapter(EntityManager em, RealmEntity realm) {
this.em = em;
@@ -1037,4 +1039,18 @@ public class RealmAdapter implements RealmModel {
em.flush();
}
+ @Override
+ public PasswordPolicy getPasswordPolicy() {
+ if (passwordPolicy == null) {
+ passwordPolicy = new PasswordPolicy(realm.getPasswordPolicy());
+ }
+ return passwordPolicy;
+ }
+
+ @Override
+ public void setPasswordPolicy(PasswordPolicy policy) {
+ this.passwordPolicy = policy;
+ realm.setPasswordPolicy(policy.toString());
+ em.flush();
+ }
}
diff --git a/model/picketlink/src/main/java/org/keycloak/models/picketlink/mappings/RealmData.java b/model/picketlink/src/main/java/org/keycloak/models/picketlink/mappings/RealmData.java
index 06e0736..ad1fbe5 100755
--- a/model/picketlink/src/main/java/org/keycloak/models/picketlink/mappings/RealmData.java
+++ b/model/picketlink/src/main/java/org/keycloak/models/picketlink/mappings/RealmData.java
@@ -27,6 +27,7 @@ public class RealmData extends AbstractPartition {
private String[] defaultRoles;
private Map<String, String> smtpConfig;
private Map<String, String> socialConfig;
+ private String passwordPolicy;
public RealmData() {
super(null);
@@ -185,4 +186,13 @@ public class RealmData extends AbstractPartition {
public void setSocialConfig(Map<String, String> socialConfig) {
this.socialConfig = socialConfig;
}
+
+ @AttributeProperty
+ public String getPasswordPolicy() {
+ return passwordPolicy;
+ }
+
+ public void setPasswordPolicy(String passwordPolicy) {
+ this.passwordPolicy = passwordPolicy;
+ }
}
diff --git a/model/picketlink/src/main/java/org/keycloak/models/picketlink/RealmAdapter.java b/model/picketlink/src/main/java/org/keycloak/models/picketlink/RealmAdapter.java
index 1300b53..798643e 100755
--- a/model/picketlink/src/main/java/org/keycloak/models/picketlink/RealmAdapter.java
+++ b/model/picketlink/src/main/java/org/keycloak/models/picketlink/RealmAdapter.java
@@ -6,6 +6,7 @@ import org.keycloak.models.ApplicationModel;
import org.keycloak.models.IdGenerator;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OAuthClientModel;
+import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredCredentialModel;
import org.keycloak.models.RoleModel;
@@ -62,6 +63,7 @@ public class RealmAdapter implements RealmModel {
protected PartitionManager partitionManager;
protected RelationshipManager relationshipManager;
protected KeycloakSession session;
+ private PasswordPolicy passwordPolicy;
public RealmAdapter(KeycloakSession session, RealmData realm, PartitionManager partitionManager) {
this.session = session;
@@ -977,4 +979,19 @@ public class RealmAdapter implements RealmModel {
realm.setSocialConfig(socialConfig);
updateRealm();
}
+
+ @Override
+ public PasswordPolicy getPasswordPolicy() {
+ if (passwordPolicy == null) {
+ passwordPolicy = new PasswordPolicy(realm.getPasswordPolicy());
+ }
+ return passwordPolicy;
+ }
+
+ @Override
+ public void setPasswordPolicy(PasswordPolicy policy) {
+ this.passwordPolicy = policy;
+ realm.setPasswordPolicy(policy.toString());
+ updateRealm();
+ }
}
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 446e86d..883cf14 100755
--- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java
@@ -5,6 +5,7 @@ import org.keycloak.models.ApplicationModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OAuthClientModel;
+import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredCredentialModel;
import org.keycloak.models.RoleModel;
@@ -110,6 +111,9 @@ public class RealmManager {
if (rep.getRequiredApplicationCredentials() != null) {
realm.updateRequiredApplicationCredentials(rep.getRequiredApplicationCredentials());
}
+
+ realm.setPasswordPolicy(new PasswordPolicy(rep.getPasswordPolicy()));
+
if (rep.getDefaultRoles() != null) {
realm.updateDefaultRoles(rep.getDefaultRoles().toArray(new String[rep.getDefaultRoles().size()]));
}
@@ -222,6 +226,8 @@ public class RealmManager {
addOAuthClientRequiredCredential(newRealm, CredentialRepresentation.PASSWORD);
}
+ newRealm.setPasswordPolicy(new PasswordPolicy(rep.getPasswordPolicy()));
+
if (rep.getUsers() != null) {
for (UserRepresentation userRep : rep.getUsers()) {
UserModel user = createUser(newRealm, userRep);
@@ -473,6 +479,9 @@ public class RealmManager {
rep.setAccessCodeLifespanUserAction(realm.getAccessCodeLifespanUserAction());
rep.setSmtpServer(realm.getSmtpConfig());
rep.setSocialProviders(realm.getSocialConfig());
+ if (realm.getPasswordPolicy() != null) {
+ rep.setPasswordPolicy(realm.getPasswordPolicy().toString());
+ }
ApplicationModel accountManagementApplication = realm.getApplicationNameMap().get(Constants.ACCOUNT_APPLICATION);
rep.setAccountManagement(accountManagementApplication != null && accountManagementApplication.isEnabled());
diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java
index 22e7269..a62a064 100755
--- a/services/src/main/java/org/keycloak/services/resources/AccountService.java
+++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java
@@ -255,6 +255,11 @@ public class AccountService {
return forms.setError(Messages.INVALID_PASSWORD_EXISTING).forwardToPassword();
}
+ String error = Validation.validatePassword(formData, realm.getPasswordPolicy());
+ if (error != null) {
+ return forms.setError(error).forwardToPassword();
+ }
+
UserCredentialModel credentials = new UserCredentialModel();
credentials.setType(CredentialRepresentation.PASSWORD);
credentials.setValue(passwordNew);
diff --git a/services/src/main/java/org/keycloak/services/resources/TokenService.java b/services/src/main/java/org/keycloak/services/resources/TokenService.java
index 580473b..5d8380d 100755
--- a/services/src/main/java/org/keycloak/services/resources/TokenService.java
+++ b/services/src/main/java/org/keycloak/services/resources/TokenService.java
@@ -297,6 +297,10 @@ public class TokenService {
}
String error = Validation.validateRegistrationForm(formData, requiredCredentialTypes);
+ if (error == null) {
+ error = Validation.validatePassword(formData, realm.getPasswordPolicy());
+ }
+
if (error != null) {
return Flows.forms(realm, request, uriInfo).setError(error).setFormData(formData)
.setSocialRegistration(isSocialRegistration).forwardToRegistration();
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 c652849..a840d08 100755
--- a/services/src/main/java/org/keycloak/services/validation/Validation.java
+++ b/services/src/main/java/org/keycloak/services/validation/Validation.java
@@ -1,5 +1,6 @@
package org.keycloak.services.validation;
+import org.keycloak.models.PasswordPolicy;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.messages.Messages;
@@ -38,6 +39,10 @@ public class Validation {
return null;
}
+ public static String validatePassword(MultivaluedMap<String, String> formData, PasswordPolicy policy) {
+ return policy.validate(formData.getFirst("password"));
+ }
+
public static String validateUpdateProfileForm(MultivaluedMap<String, String> formData) {
if (isEmpty(formData.getFirst("firstName"))) {
return Messages.MISSING_FIRST_NAME;
diff --git a/services/src/test/java/org/keycloak/test/ModelTest.java b/services/src/test/java/org/keycloak/test/ModelTest.java
index 6771e29..5f0728a 100755
--- a/services/src/test/java/org/keycloak/test/ModelTest.java
+++ b/services/src/test/java/org/keycloak/test/ModelTest.java
@@ -6,6 +6,7 @@ import org.junit.Before;
import org.junit.Test;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.representations.idm.RealmRepresentation;
@@ -46,6 +47,7 @@ public class ModelTest extends AbstractKeycloakServerTest {
realm.setSslNotRequired(true);
realm.setVerifyEmail(true);
realm.setTokenLifespan(1000);
+ realm.setPasswordPolicy(new PasswordPolicy("length"));
realm.setAccessCodeLifespan(1001);
realm.setAccessCodeLifespanUserAction(1002);
realm.setPublicKeyPem("0234234");
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/AccountTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/AccountTest.java
index 17d6348..020a838 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/AccountTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/AccountTest.java
@@ -164,6 +164,36 @@ public class AccountTest {
}
@Test
+ public void changePasswordWithPasswordPolicy() {
+ keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ appRealm.setPasswordPolicy(new PasswordPolicy("length"));
+ }
+ });
+
+ try {
+ changePasswordPage.open();
+ loginPage.login("test-user@localhost", "password");
+
+ changePasswordPage.changePassword("", "new", "new");
+
+ Assert.assertTrue(profilePage.isError());
+
+ changePasswordPage.changePassword("password", "new-password", "new-password");
+
+ Assert.assertTrue(profilePage.isSuccess());
+ } finally {
+ keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ appRealm.setPasswordPolicy(new PasswordPolicy(null));
+ }
+ });
+ }
+ }
+
+ @Test
public void changeProfile() {
profilePage.open();
loginPage.login("test-user@localhost", "password");
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java
index e82b31f..cd7795e 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java
@@ -25,6 +25,9 @@ import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
+import org.keycloak.models.PasswordPolicy;
+import org.keycloak.models.RealmModel;
+import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.pages.LoginPage;
@@ -94,6 +97,37 @@ public class RegisterTest {
}
@Test
+ public void registerPasswordPolicy() {
+ keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ appRealm.setPasswordPolicy(new PasswordPolicy("length"));
+ }
+ });
+
+ try {
+ loginPage.open();
+ loginPage.clickRegister();
+ registerPage.assertCurrent();
+
+ registerPage.register("firstName", "lastName", "email", "registerPasswordPolicy", "pass", "pass");
+
+ registerPage.assertCurrent();
+ Assert.assertEquals("Invalid password: minimum length 8", registerPage.getError());
+
+ registerPage.register("firstName", "lastName", "email", "registerPasswordPolicy", "password", "password");
+ Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+ } finally {
+ keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ appRealm.setPasswordPolicy(new PasswordPolicy(null));
+ }
+ });
+ }
+ }
+
+ @Test
public void registerUserMissingUsername() {
loginPage.open();
loginPage.clickRegister();
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/RegisterPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/RegisterPage.java
index c1b52c5..c67d692 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/RegisterPage.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/RegisterPage.java
@@ -54,26 +54,32 @@ public class RegisterPage extends AbstractPage {
private WebElement loginErrorMessage;
public void register(String firstName, String lastName, String email, String username, String password, String passwordConfirm) {
+ firstNameInput.clear();
if (firstName != null) {
firstNameInput.sendKeys(firstName);
}
+ lastNameInput.clear();
if (lastName != null) {
lastNameInput.sendKeys(lastName);
}
+ emailInput.clear();
if (email != null) {
emailInput.sendKeys(email);
}
+ usernameInput.clear();
if (username != null) {
usernameInput.sendKeys(username);
}
+ passwordInput.clear();
if (password != null) {
passwordInput.sendKeys(password);
}
+ passwordConfirmInput.clear();
if (passwordConfirm != null) {
passwordConfirmInput.sendKeys(passwordConfirm);
}