From ca13e3c4ba3a3fe08b2c4dd712218683239fb09e Mon Sep 17 00:00:00 2001 From: girirajsharma Date: Wed, 1 Apr 2015 18:06:56 +0530 Subject: [PATCH] [KEYCLOAK-400]Provide a configuration for regex in the password policies --- .../modules/security-vulnerabilities.xml | 4 +- .../theme/base/admin/resources/js/services.js | 26 +++++++---- .../resources/partials/realm-credentials.html | 5 +-- .../org/keycloak/models/PasswordPolicy.java | 45 ++++++++++++++++--- .../resources/admin/RealmAdminResource.java | 8 +++- 5 files changed, 68 insertions(+), 20 deletions(-) diff --git a/docbook/reference/en/en-US/modules/security-vulnerabilities.xml b/docbook/reference/en/en-US/modules/security-vulnerabilities.xml index 76bc36b5e2..359c55e5f3 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 @@ 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 6dbe6b8cd5..d7833249cd 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 608a2b622f..552b049c49 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 @@
- Realm Password Policy + Realm Password Policy @@ -47,8 +47,7 @@
+ placeholder="No value assigned" min="1" required>
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 cfcc108db8..703e9c6593 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 Stian Thorgersen @@ -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 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 a019e6bc34..34cd97eaf0 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."); } }