[KEYCLOAK-400]Provide a configuration for regex in the password policies
This commit is contained in:
parent
3a5861c624
commit
ca13e3c4ba
5 changed files with 68 additions and 20 deletions
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,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_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;
|
||||
|
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue