keycloak-uncached

KEYCLOAK-186 Password policies

12/3/2013 10:53:26 AM

Changes

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>
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);
         }