Merge pull request #1113 from girirajsharma/master

[KEYCLOAK-400]Provide a configuration for regex in the password policies
This commit is contained in:
Bill Burke 2015-04-03 15:00:57 -04:00
commit 463310e1c3
5 changed files with 68 additions and 20 deletions

View file

@ -127,7 +127,9 @@
</para> </para>
<para> <para>
In the admin console, per realm, you can set up a password policy to enforce that users pick hard to guess passwords. 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 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 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 the legitimate parties. Increasing n also slightly increases the odds that a random password gives the same result as the right

View file

@ -1043,7 +1043,8 @@ module.factory('PasswordPolicy', function() {
lowerCase: "Minimal number (integer type) of lowercase characters 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.", 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.", 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 = [ p.allPolicies = [
@ -1053,11 +1054,13 @@ module.factory('PasswordPolicy', function() {
{ name: 'lowerCase', value: 1 }, { name: 'lowerCase', value: 1 },
{ name: 'upperCase', value: 1 }, { name: 'upperCase', value: 1 },
{ name: 'specialChars', value: 1 }, { name: 'specialChars', value: 1 },
{ name: 'notUsername', value: 1 } { name: 'notUsername', value: 1 },
{ name: 'regexPatterns', value: ''}
]; ];
p.parse = function(policyString) { p.parse = function(policyString) {
var policies = []; var policies = [];
var re, policyEntry;
if (!policyString || policyString.length == 0){ if (!policyString || policyString.length == 0){
return policies; return policies;
@ -1067,14 +1070,21 @@ module.factory('PasswordPolicy', function() {
for (var i = 0; i < policyArray.length; i ++){ for (var i = 0; i < policyArray.length; i ++){
var policyToken = policyArray[i]; var policyToken = policyArray[i];
var re = /(\w+)\(*(\d*)\)*/;
var policyEntry = re.exec(policyToken); 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) { if (null !== policyEntry) {
policies.push({ name: policyEntry[1], value: parseInt(policyEntry[2]) }); policies.push({ name: policyEntry[1], value: parseInt(policyEntry[2]) });
} }
} }
}
return policies; return policies;
}; };

View file

@ -17,7 +17,7 @@
</div> </div>
</fieldset> </fieldset>
<fieldset class="border-top"> <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"> <table class="table table-striped table-bordered">
<caption class="hidden">Table of Password Policies</caption> <caption class="hidden">Table of Password Policies</caption>
<thead> <thead>
@ -47,8 +47,7 @@
</td> </td>
<td> <td>
<input class="form-control" ng-model="p.value" ng-show="p.name != 'notUsername' " <input class="form-control" ng-model="p.value" ng-show="p.name != 'notUsername' "
placeholder="No value assigned" placeholder="No value assigned" min="1" required>
min="1">
</td> </td>
<td class="actions"> <td class="actions">
<div class="action-div"><i class="pficon pficon-delete" ng-click="removePolicy($index)" tooltip-placement="right" tooltip="Remove Policy"></i></div> <div class="action-div"><i class="pficon pficon-delete" ng-click="removePolicy($index)" tooltip-placement="right" tooltip="Remove Policy"></i></div>

View file

@ -1,9 +1,10 @@
package org.keycloak.models; package org.keycloak.models;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -16,6 +17,7 @@ public class PasswordPolicy {
public static final String INVALID_PASSWORD_MIN_UPPER_CASE_CHARS_MESSAGE = "invalidPasswordMinUpperCaseCharsMessage"; 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_MIN_SPECIAL_CHARS_MESSAGE = "invalidPasswordMinSpecialCharsMessage";
public static final String INVALID_PASSWORD_NOT_USERNAME = "invalidPasswordNotUsernameMessage"; public static final String INVALID_PASSWORD_NOT_USERNAME = "invalidPasswordNotUsernameMessage";
public static final String INVALID_PASSWORD_REGEX_PATTERN = "invalidPasswordRegexPatternMessage";
private List<Policy> policies; private List<Policy> policies;
private String policyString; private String policyString;
@ -64,6 +66,11 @@ public class PasswordPolicy {
list.add(new NotUsername(args)); list.add(new NotUsername(args));
} else if (name.equals(HashIterations.NAME)) { } else if (name.equals(HashIterations.NAME)) {
list.add(new HashIterations(args)); 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; return list;
@ -74,10 +81,11 @@ public class PasswordPolicy {
* @return -1 if no hash iterations setting * @return -1 if no hash iterations setting
*/ */
public int getHashIterations() { public int getHashIterations() {
if (policies == null) return -1; if (policies == null)
return -1;
for (Policy p : policies) { for (Policy p : policies) {
if (p instanceof HashIterations) { 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 Error validate(String username, String password);
} }
public static class Error{ public static class Error {
private String message; private String message;
private Object[] parameters; private Object[] parameters;
private Error(String message, Object ... parameters){ private Error(String message, Object... parameters) {
this.message = message; this.message = message;
this.parameters = parameters; this.parameters = parameters;
} }
@ -192,7 +200,7 @@ public class PasswordPolicy {
count++; 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) { private static int intArg(String policy, int defaultValue, String... args) {
if (args == null || args.length == 0) { if (args == null || args.length == 0) {
return defaultValue; return defaultValue;

View file

@ -46,10 +46,12 @@ import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.regex.PatternSyntaxException;
/** /**
* Base resource class for the admin REST api of one realm * Base resource class for the admin REST api of one realm
@ -218,8 +220,12 @@ public class RealmAdminResource {
} }
return Response.noContent().build(); return Response.noContent().build();
} catch (PatternSyntaxException e) {
return Flows.errors().exists("Specified regex pattern(s) is invalid.");
} catch (ModelDuplicateException e) { } 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.");
} }
} }