[KEYCLOAK-400]Provide a configuration for regex in the password policies

This commit is contained in:
girirajsharma 2015-04-01 18:06:56 +05:30
parent 3a5861c624
commit ca13e3c4ba
5 changed files with 68 additions and 20 deletions

View file

@ -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

View file

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

View file

@ -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>

View file

@ -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;

View file

@ -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.");
}
}