keycloak-aplcache

Details

diff --git a/docbook/reference/en/en-US/modules/security-vulnerabilities.xml b/docbook/reference/en/en-US/modules/security-vulnerabilities.xml
index 76bc36b..359c55e 100755
--- a/docbook/reference/en/en-US/modules/security-vulnerabilities.xml
+++ b/docbook/reference/en/en-US/modules/security-vulnerabilities.xml
@@ -127,7 +127,9 @@
         </para>
         <para>
             In the admin console, per realm, you can set up a password policy to enforce that users pick hard to guess passwords.
-            The password policies that can be configured are Hash Iterations, length, digits, lowercase, uppercase and special characters.
+            A password has to match all policies. The password policies that can be configured are hash iterations, length, digits,
+            lowercase, uppercase, special characters, not username and regex patterns. Multiple regex patterns, separated by comma,
+            can be specified. If there's more than one regex added, password has to match all fully.
             Increasing number of Hash Iterations (n) does not worsen anything (and certainly not the cipher) and it greatly increases the 
             resistance to dictionary attacks. However the drawback to increasing n is that it has some cost (CPU usage, energy, delay) for 
             the legitimate parties. Increasing n also slightly increases the odds that a random password gives the same result as the right 
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js
index 6dbe6b8..d783324 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js
@@ -1043,7 +1043,8 @@ module.factory('PasswordPolicy', function() {
         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"
+        notUsername:    "Block passwords that are equal to the username",
+        regexPatterns:  "Block passwords that do not match all of the regex patterns (string type)."
     }
 
     p.allPolicies = [
@@ -1053,11 +1054,13 @@ module.factory('PasswordPolicy', function() {
         { name: 'lowerCase', value: 1 },
         { name: 'upperCase', value: 1 },
         { name: 'specialChars', value: 1 },
-        { name: 'notUsername', value: 1 }
+        { name: 'notUsername', value: 1 },
+        { name: 'regexPatterns', value: ''}
     ];
 
     p.parse = function(policyString) {
         var policies = [];
+        var re, policyEntry;
 
         if (!policyString || policyString.length == 0){
             return policies;
@@ -1067,14 +1070,21 @@ module.factory('PasswordPolicy', function() {
 
         for (var i = 0; i < policyArray.length; i ++){
             var policyToken = policyArray[i];
-            var re = /(\w+)\(*(\d*)\)*/;
-
-            var policyEntry = re.exec(policyToken);
-            if (null !== policyEntry) {
-                policies.push({ name: policyEntry[1], value: parseInt(policyEntry[2]) });
+            
+            if(policyToken.indexOf('regexPatterns') === 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;
     };
 
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-credentials.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-credentials.html
index 608a2b6..552b049 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-credentials.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-credentials.html
@@ -17,7 +17,7 @@
                 </div>
             </fieldset>
             <fieldset class="border-top">
-                <legend><span class="text">Realm Password Policy</span> <span tooltip-placement="right" tooltip="Specify required password format.  You can also set how many times a password is hashed before it is stored in database." class="fa fa-info-circle"></span></legend>
+                <legend><span class="text">Realm Password Policy</span> <span tooltip-placement="right" tooltip="Specify required password format.  You can also set how many times a password is hashed before it is stored in database. Multiple Regex patterns, separated by comma, can be added." class="fa fa-info-circle"></span></legend>
                 <table class="table table-striped table-bordered">
                     <caption class="hidden">Table of Password Policies</caption>
                     <thead>
@@ -47,8 +47,7 @@
                         </td>
                         <td>
                             <input class="form-control" ng-model="p.value" ng-show="p.name != 'notUsername' "
-                                   placeholder="No value assigned"
-                                   min="1">
+                                   placeholder="No value assigned" min="1" required>
                         </td>
                         <td class="actions">
                             <div class="action-div"><i class="pficon pficon-delete" ng-click="removePolicy($index)" tooltip-placement="right" tooltip="Remove Policy"></i></div>
diff --git a/model/api/src/main/java/org/keycloak/models/PasswordPolicy.java b/model/api/src/main/java/org/keycloak/models/PasswordPolicy.java
index cfcc108..703e9c6 100755
--- a/model/api/src/main/java/org/keycloak/models/PasswordPolicy.java
+++ b/model/api/src/main/java/org/keycloak/models/PasswordPolicy.java
@@ -1,9 +1,10 @@
 package org.keycloak.models;
 
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -16,7 +17,8 @@ public class PasswordPolicy {
     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";
+
     private List<Policy> policies;
     private String policyString;
 
@@ -64,6 +66,11 @@ public class PasswordPolicy {
                 list.add(new NotUsername(args));
             } else if (name.equals(HashIterations.NAME)) {
                 list.add(new HashIterations(args));
+            } else if (name.equals(RegexPatterns.NAME)) {
+                for(String regexPattern : args) {
+                    Pattern.compile(regexPattern);
+                }
+                list.add(new RegexPatterns(args));
             }
         }
         return list;
@@ -74,10 +81,11 @@ public class PasswordPolicy {
      * @return -1 if no hash iterations setting
      */
     public int getHashIterations() {
-        if (policies == null) return -1;
+        if (policies == null)
+            return -1;
         for (Policy p : policies) {
             if (p instanceof HashIterations) {
-                return ((HashIterations)p).iterations;
+                return ((HashIterations) p).iterations;
             }
 
         }
@@ -98,11 +106,11 @@ public class PasswordPolicy {
         public Error validate(String username, String password);
     }
 
-    public static class Error{
+    public static class Error {
         private String message;
         private Object[] parameters;
 
-        private Error(String message, Object ... parameters){
+        private Error(String message, Object... parameters) {
             this.message = message;
             this.parameters = parameters;
         }
@@ -192,7 +200,7 @@ public class PasswordPolicy {
                     count++;
                 }
             }
-            return count < min ? new Error(INVALID_PASSWORD_MIN_LOWER_CASE_CHARS_MESSAGE, min): null;
+            return count < min ? new Error(INVALID_PASSWORD_MIN_LOWER_CASE_CHARS_MESSAGE, min) : null;
         }
     }
 
@@ -236,6 +244,29 @@ public class PasswordPolicy {
         }
     }
 
+    private static class RegexPatterns implements Policy {
+        private static final String NAME = "regexPatterns";
+        private String regexPatterns[];
+
+        public RegexPatterns(String[] args) {
+            regexPatterns = args;
+        }
+
+        @Override
+        public Error validate(String username, String password) {
+            Pattern pattern = null;
+            Matcher matcher = null;
+            for(String regexPattern : regexPatterns) {
+                pattern = Pattern.compile(regexPattern);
+                matcher = pattern.matcher(password);
+                if (!matcher.matches()) {
+                    return new Error(INVALID_PASSWORD_REGEX_PATTERN, (Object)regexPatterns);
+                }
+            }
+            return null;
+        }
+    }
+
     private static int intArg(String policy, int defaultValue, String... args) {
         if (args == null || args.length == 0) {
             return defaultValue;
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 a019e6b..34cd97e 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
@@ -46,10 +46,12 @@ import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriInfo;
+
 import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.regex.PatternSyntaxException;
 
 /**
  * Base resource class for the admin REST api of one realm
@@ -218,8 +220,12 @@ public class RealmAdminResource {
             }
 
             return Response.noContent().build();
+        } catch (PatternSyntaxException e) {
+            return Flows.errors().exists("Specified regex pattern(s) is invalid.");
         } catch (ModelDuplicateException e) {
-            return Flows.errors().exists("Realm " + rep.getRealm() + " already exists");
+            return Flows.errors().exists("Realm " + rep.getRealm() + " already exists.");
+        }  catch (Exception e) {
+            return Flows.errors().exists("Failed to update " + rep.getRealm() + " Realm.");
         }
     }