Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
a42a750ebb
9 changed files with 90 additions and 23 deletions
|
@ -38,7 +38,11 @@
|
||||||
<groupId>org.jboss.logging</groupId>
|
<groupId>org.jboss.logging</groupId>
|
||||||
<artifactId>jboss-logging</artifactId>
|
<artifactId>jboss-logging</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-services</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|
|
@ -15,14 +15,18 @@ import liquibase.resource.ResourceAccessor;
|
||||||
import liquibase.snapshot.SnapshotGeneratorFactory;
|
import liquibase.snapshot.SnapshotGeneratorFactory;
|
||||||
import liquibase.statement.SqlStatement;
|
import liquibase.statement.SqlStatement;
|
||||||
import liquibase.structure.core.Table;
|
import liquibase.structure.core.Table;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.connections.jpa.updater.liquibase.ThreadLocalSessionContext;
|
import org.keycloak.connections.jpa.updater.liquibase.ThreadLocalSessionContext;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.services.DefaultKeycloakSessionFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
*/
|
*/
|
||||||
public abstract class CustomKeycloakTask implements CustomSqlChange {
|
public abstract class CustomKeycloakTask implements CustomSqlChange {
|
||||||
|
|
||||||
|
private final Logger logger = Logger.getLogger(getClass());
|
||||||
|
|
||||||
protected KeycloakSession kcSession;
|
protected KeycloakSession kcSession;
|
||||||
|
|
||||||
protected Database database;
|
protected Database database;
|
||||||
|
@ -49,8 +53,18 @@ public abstract class CustomKeycloakTask implements CustomSqlChange {
|
||||||
@Override
|
@Override
|
||||||
public void setUp() throws SetupException {
|
public void setUp() throws SetupException {
|
||||||
this.kcSession = ThreadLocalSessionContext.getCurrentSession();
|
this.kcSession = ThreadLocalSessionContext.getCurrentSession();
|
||||||
|
|
||||||
if (this.kcSession == null) {
|
if (this.kcSession == null) {
|
||||||
throw new SetupException("No KeycloakSession provided in ThreadLocal");
|
// Probably running Liquibase from maven plugin. Try to create kcSession programmatically
|
||||||
|
logger.info("No KeycloakSession provided in ThreadLocal. Initializing KeycloakSessionFactory");
|
||||||
|
|
||||||
|
try {
|
||||||
|
DefaultKeycloakSessionFactory factory = new DefaultKeycloakSessionFactory();
|
||||||
|
factory.init();
|
||||||
|
this.kcSession = factory.create();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new SetupException("Exception when initializing factory", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
<module name="org.keycloak.keycloak-core"/>
|
<module name="org.keycloak.keycloak-core"/>
|
||||||
<module name="org.keycloak.keycloak-model-api"/>
|
<module name="org.keycloak.keycloak-model-api"/>
|
||||||
<module name="org.keycloak.keycloak-connections-jpa"/>
|
<module name="org.keycloak.keycloak-connections-jpa"/>
|
||||||
|
<module name="org.keycloak.keycloak-services"/>
|
||||||
<module name="org.liquibase"/>
|
<module name="org.liquibase"/>
|
||||||
<module name="javax.persistence.api"/>
|
<module name="javax.persistence.api"/>
|
||||||
<module name="org.jboss.logging"/>
|
<module name="org.jboss.logging"/>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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*)\)*/;
|
|
||||||
|
if(policyToken.indexOf('regexPatterns') === 0) {
|
||||||
var policyEntry = re.exec(policyToken);
|
re = /(\w+)\((.*)\)/;
|
||||||
if (null !== policyEntry) {
|
policyEntry = re.exec(policyToken);
|
||||||
policies.push({ name: policyEntry[1], value: parseInt(policyEntry[2]) });
|
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;
|
return policies;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -56,7 +56,7 @@ public class CatalinaUserSessionManagement implements SessionListener {
|
||||||
try {
|
try {
|
||||||
session.expire();
|
session.expire();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warnf("Session not present or already invalidated.");
|
log.warn("Session not present or already invalidated.", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,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_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;
|
||||||
|
|
|
@ -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.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue