KEYCLOAK-5244 Add BlacklistPasswordPolicyProvider (#4370)
* KEYCLOAK-5244 Add BlacklistPasswordPolicyProvider This introduces a new PasswordPolicy which can refer to a named predefined password-blacklist to avoid users choosing too easy to guess passwords. The BlacklistPasswordPolicyProvider supports built-in as well as custom blacklists. built-in blacklists use the form `default/filename` and custom ones `custom/filename`, where filename is the name of the found blacklist-filename. I'd propose to use some of the freely available password blacklists from the [SecLists](https://github.com/danielmiessler/SecLists/tree/master/Passwords) project. For testing purposes one can download the password blacklist ``` wget -O 10_million_password_list_top_1000000.txt https://github.com/danielmiessler/SecLists/blob/master/Passwords/10_million_password_list_top_1000000.txt?raw=true ``` to /data/keycloak/blacklists/ Custom password policies can be configured with the SPI configuration mechanism via jboss-cli: ``` /subsystem=keycloak-server/spi=password-policy:add() /subsystem=keycloak-server/spi=password-policy/provider=passwordBlacklist:add(enabled=true) /subsystem=keycloak-server/spi=password-policy/provider=passwordBlacklist:write-attribute(name=properties.blacklistsFolderUri, value=file:///data/keycloak/blacklists/) ``` Password blacklist is stored in a TreeSet. * KEYCLOAK-5244 Encode PasswordBlacklist as a BloomFilter We now use a dynamically sized BloomFilter with a false positive probability of 1% as a backing store for PasswordBlacklists. BloomFilter implementation is provided by google-guava which is available in wildfly. Password blacklist files are now resolved against the ${jboss.server.data.dir}/password-blacklists. This can be overridden via system property, or SPI config. See JavaDoc of BlacklistPasswordPolicyProviderFactory for details. Revised implementation to be more extensible, e.g. it could be possible to use other stores like databases etc. Moved FileSystem specific methods to FileBasesPasswordBlacklistPolicy. The PasswordBlacklistProvider uses the guava version 20.0 shipped with wildfly. Unfortunately the arquillian testsuite transitively depends on guava 23.0 via the selenium-3.5.1 dependency. Hence we need to use version 23.0 for tests but 20.0 for the policy provider to avoid NoClassDefFoundErrors in the server-dist. Configure password blacklist folder for tests * KEYCLOAK-5244 Configure jboss.server.data.dir for test servers * KEYCLOAK-5244 Translate blacklisted message in base/login
This commit is contained in:
parent
fe76b2428b
commit
3103e0fd0a
17 changed files with 515 additions and 9 deletions
|
@ -38,5 +38,6 @@
|
|||
<module name="javax.transaction.api"/>
|
||||
<module name="com.fasterxml.jackson.core.jackson-databind"/>
|
||||
<module name="com.fasterxml.jackson.core.jackson-core"/>
|
||||
<module name="com.google.guava"/>
|
||||
</dependencies>
|
||||
</module>
|
||||
|
|
10
pom.xml
10
pom.xml
|
@ -91,6 +91,10 @@
|
|||
<apacheds.version>2.0.0-M21</apacheds.version>
|
||||
<apacheds.codec.version>1.0.0-M33</apacheds.codec.version>
|
||||
<google.zxing.version>3.2.1</google.zxing.version>
|
||||
|
||||
<!-- Same version as ships with wildfly. -->
|
||||
<google.guava.version>20.0</google.guava.version>
|
||||
|
||||
<freemarker.version>2.3.23</freemarker.version>
|
||||
<jetty9.version>9.1.0.v20131115</jetty9.version>
|
||||
<liquibase.version>3.4.1</liquibase.version>
|
||||
|
@ -440,6 +444,12 @@
|
|||
<version>${google.zxing.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>${google.guava.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Email Test Servers -->
|
||||
<dependency>
|
||||
<groupId>com.icegreen</groupId>
|
||||
|
|
|
@ -76,6 +76,11 @@
|
|||
<artifactId>httpclient</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
package org.keycloak.policy;
|
||||
|
||||
import org.keycloak.models.KeycloakContext;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.policy.BlacklistPasswordPolicyProviderFactory.FileBasedPasswordBlacklist;
|
||||
import org.keycloak.policy.BlacklistPasswordPolicyProviderFactory.PasswordBlacklist;
|
||||
|
||||
/**
|
||||
* Checks a password against a configured password blacklist.
|
||||
*
|
||||
* @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
|
||||
*/
|
||||
public class BlacklistPasswordPolicyProvider implements PasswordPolicyProvider {
|
||||
|
||||
public static final String ERROR_MESSAGE = "invalidPasswordBlacklistedMessage";
|
||||
|
||||
private final KeycloakContext context;
|
||||
|
||||
private final BlacklistPasswordPolicyProviderFactory factory;
|
||||
|
||||
public BlacklistPasswordPolicyProvider(KeycloakContext context, BlacklistPasswordPolicyProviderFactory factory) {
|
||||
this.context = context;
|
||||
this.factory = factory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the provided password is contained in the configured blacklist.
|
||||
*
|
||||
* @param username
|
||||
* @param password
|
||||
* @return {@literal null} if the password is not blacklisted otherwise a {@link PolicyError}
|
||||
*/
|
||||
@Override
|
||||
public PolicyError validate(String username, String password) {
|
||||
|
||||
Object policyConfig = context.getRealm().getPasswordPolicy().getPolicyConfig(BlacklistPasswordPolicyProviderFactory.ID);
|
||||
if (policyConfig == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(policyConfig instanceof PasswordBlacklist)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
PasswordBlacklist blacklist = (FileBasedPasswordBlacklist) policyConfig;
|
||||
|
||||
if (!blacklist.contains(password)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new PolicyError(ERROR_MESSAGE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PolicyError validate(RealmModel realm, UserModel user, String password) {
|
||||
return validate(user.getUsername(), password);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object parseConfig(String blacklistName) {
|
||||
|
||||
if (blacklistName == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return factory.resolvePasswordBlacklist(blacklistName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
//noop
|
||||
}
|
||||
}
|
|
@ -0,0 +1,331 @@
|
|||
/*
|
||||
* Copyright 2017 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.policy;
|
||||
|
||||
import com.google.common.hash.BloomFilter;
|
||||
import com.google.common.hash.Funnels;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
/**
|
||||
* Creates {@link BlacklistPasswordPolicyProvider} instances.
|
||||
* <p>
|
||||
* Password blacklists are simple text files where every line is a blacklisted password delimited by {@code \n}.
|
||||
* Blacklist files are discovered and registered at startup.
|
||||
* <p>Blacklists can be configured via the <em>Authentication: Password Policy</em> section in the admin-console.
|
||||
* A blacklist-file is referred to by its name in the policy configuration.
|
||||
* <p>Users can provide custom blacklists by adding a blacklist password file to the configured blacklist folder.
|
||||
* <p>
|
||||
* <p>The location of the password-blacklists folder is derived as follows</p>
|
||||
* <ol>
|
||||
* <li>the value of the System property {@code keycloak.password.blacklists.path} if configured - fails if folder is missing</li>
|
||||
* <li>the value of the SPI config property: {@code blacklistsPath} when explicitly configured - fails if folder is missing</li>
|
||||
* <li>otherwise {@code ${jboss.server.data.dir}/password-blacklists/} if nothing else is configured - the folder is created automatically if not present</li>
|
||||
* </ol>
|
||||
* <p>Note that the preferred way for configuration is to copy the password file to the {@code ${jboss.server.data.dir}/password-blacklists/} folder</p>
|
||||
* <p>To configure a password blacklist via the SPI configuration, run the following jboss-cli script:</p>
|
||||
* <pre>{@code
|
||||
* /subsystem=keycloak-server/spi=password-policy:add()
|
||||
* /subsystem=keycloak-server/spi=password-policy/provider=passwordBlacklist:add(enabled=true)
|
||||
* /subsystem=keycloak-server/spi=password-policy/provider=passwordBlacklist:write-attribute(name=properties.blacklistsPath, value=/data/keycloak/blacklists/)
|
||||
* }</pre>
|
||||
* <p>A password blacklist with the filename {@code 10_million_password_list_top_1000000-password-blacklist.txt}
|
||||
* that is located beneath {@code /data/keycloak/blacklists/} can be referred to
|
||||
* as {@code 10_million_password_list_top_1000000-password-blacklist.txt} in the <em>Authentication: Password Policy</em> configuration.
|
||||
*
|
||||
* @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
|
||||
*/
|
||||
public class BlacklistPasswordPolicyProviderFactory implements PasswordPolicyProviderFactory {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(BlacklistPasswordPolicyProviderFactory.class);
|
||||
|
||||
public static final String ID = "passwordBlacklist";
|
||||
|
||||
public static final String SYSTEM_PROPERTY = "keycloak.password.blacklists.path";
|
||||
|
||||
public static final String BLACKLISTS_PATH_PROPERTY = "blacklistsPath";
|
||||
|
||||
public static final String JBOSS_SERVER_DATA_DIR = "jboss.server.data.dir";
|
||||
|
||||
public static final String PASSWORD_BLACKLISTS_FOLDER = "password-blacklists/";
|
||||
|
||||
private ConcurrentMap<String, FileBasedPasswordBlacklist> blacklistRegistry = new ConcurrentHashMap<>();
|
||||
|
||||
private Path blacklistsBasePath;
|
||||
|
||||
@Override
|
||||
public PasswordPolicyProvider create(KeycloakSession session) {
|
||||
return new BlacklistPasswordPolicyProvider(session.getContext(), this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
this.blacklistsBasePath = FileBasedPasswordBlacklist.detectBlacklistsBasePath(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return "Password Blacklist";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getConfigType() {
|
||||
return PasswordPolicyProvider.STRING_CONFIG_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDefaultConfigValue() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMultiplSupported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Resolves and potentially registers a {@link PasswordBlacklist} for the given {@code blacklistName}.
|
||||
*
|
||||
* @param blacklistName
|
||||
* @return
|
||||
*/
|
||||
public PasswordBlacklist resolvePasswordBlacklist(String blacklistName) {
|
||||
|
||||
Objects.requireNonNull(blacklistName, "blacklistName");
|
||||
|
||||
String cleanedBlacklistName = blacklistName.trim();
|
||||
if (cleanedBlacklistName.isEmpty()) {
|
||||
throw new IllegalArgumentException("Password blacklist name must not be empty!");
|
||||
}
|
||||
|
||||
return blacklistRegistry.computeIfAbsent(cleanedBlacklistName, (name) -> {
|
||||
FileBasedPasswordBlacklist pbl = new FileBasedPasswordBlacklist(this.blacklistsBasePath, name);
|
||||
pbl.lazyInit();
|
||||
return pbl;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link PasswordBlacklist} describes a list of too easy to guess
|
||||
* or potentially leaked passwords that users should not be able to use.
|
||||
*/
|
||||
public interface PasswordBlacklist {
|
||||
|
||||
|
||||
/**
|
||||
* @return the logical name of the {@link PasswordBlacklist}
|
||||
*/
|
||||
String getName();
|
||||
|
||||
/**
|
||||
* Checks whether a given {@code password} is contained in this {@link PasswordBlacklist}.
|
||||
*
|
||||
* @param password
|
||||
* @return
|
||||
*/
|
||||
boolean contains(String password);
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link FileBasedPasswordBlacklist} uses password-blacklist files as
|
||||
* to construct a {@link PasswordBlacklist}.
|
||||
* <p>
|
||||
* This implementation uses a dynamically sized {@link BloomFilter}
|
||||
* to provide a false positive probability of 1%.
|
||||
*
|
||||
* @see BloomFilter
|
||||
*/
|
||||
public static class FileBasedPasswordBlacklist implements PasswordBlacklist {
|
||||
|
||||
private static final double FALSE_POSITIVE_PROBABILITY = 0.01;
|
||||
|
||||
private static final int BUFFER_SIZE_IN_BYTES = 512 * 1024;
|
||||
|
||||
/**
|
||||
* The name of the blacklist filename.
|
||||
*/
|
||||
private final String name;
|
||||
|
||||
/**
|
||||
* The concrete path to the password-blacklist file.
|
||||
*/
|
||||
private final Path path;
|
||||
|
||||
/**
|
||||
* Initialized lazily via {@link #lazyInit()}
|
||||
*/
|
||||
private BloomFilter<String> blacklist;
|
||||
|
||||
public FileBasedPasswordBlacklist(Path blacklistBasePath, String name) {
|
||||
|
||||
this.name = name;
|
||||
this.path = blacklistBasePath.resolve(name);
|
||||
|
||||
|
||||
if (name.contains("/")) {
|
||||
// disallow '/' to avoid accidental filesystem traversal
|
||||
throw new IllegalArgumentException("" + name + " must not contain slashes!");
|
||||
}
|
||||
|
||||
if (!Files.exists(this.path)) {
|
||||
throw new IllegalArgumentException("Password blacklist " + name + " not found!");
|
||||
}
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public boolean contains(String password) {
|
||||
return blacklist != null && blacklist.mightContain(password);
|
||||
}
|
||||
|
||||
void lazyInit() {
|
||||
|
||||
if (blacklist != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.blacklist = load();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the referenced blacklist into a {@link BloomFilter}.
|
||||
*
|
||||
* @return the {@link BloomFilter} backing a password blacklist
|
||||
*/
|
||||
private BloomFilter<String> load() {
|
||||
|
||||
try {
|
||||
LOG.infof("Loading blacklist with name %s from %s - start", name, path);
|
||||
|
||||
long passwordCount = getPasswordCount();
|
||||
|
||||
BloomFilter<String> filter = BloomFilter.create(
|
||||
Funnels.stringFunnel(StandardCharsets.UTF_8),
|
||||
passwordCount,
|
||||
FALSE_POSITIVE_PROBABILITY);
|
||||
|
||||
try (BufferedReader br = newReader(path)) {
|
||||
br.lines().forEach(filter::put);
|
||||
}
|
||||
|
||||
LOG.infof("Loading blacklist with name %s from %s - end", name, path);
|
||||
|
||||
return filter;
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Could not load password blacklist from path: " + path, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines password blacklist size to correctly size the {@link BloomFilter} backing this blacklist.
|
||||
*
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
private long getPasswordCount() throws IOException {
|
||||
|
||||
/*
|
||||
* TODO find a more efficient way to determine the password count,
|
||||
* e.g. require a header-line in the password-blacklist file
|
||||
*/
|
||||
try (BufferedReader br = newReader(path)) {
|
||||
return br.lines().count();
|
||||
}
|
||||
}
|
||||
|
||||
private static BufferedReader newReader(Path path) throws IOException {
|
||||
return new BufferedReader(Files.newBufferedReader(path), BUFFER_SIZE_IN_BYTES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Discovers password blacklists location.
|
||||
* <p>
|
||||
* <ol>
|
||||
* <li>
|
||||
* system property {@code keycloak.password.blacklists.path} if present
|
||||
* </li>
|
||||
* <li>SPI config property {@code blacklistsPath}</li>
|
||||
* </ol>
|
||||
* and fallback to the {@code /data/password-blacklists} folder of the currently
|
||||
* running wildfly instance.
|
||||
*
|
||||
* @param config
|
||||
* @return the detected blacklist path
|
||||
* @throws IllegalStateException if no blacklist folder could be detected
|
||||
*/
|
||||
private static Path detectBlacklistsBasePath(Config.Scope config) {
|
||||
|
||||
String pathFromSysProperty = System.getProperty(SYSTEM_PROPERTY);
|
||||
if (pathFromSysProperty != null) {
|
||||
return ensureExists(Paths.get(pathFromSysProperty));
|
||||
}
|
||||
|
||||
String pathFromSpiConfig = config.get(BLACKLISTS_PATH_PROPERTY);
|
||||
if (pathFromSpiConfig != null) {
|
||||
return ensureExists(Paths.get(pathFromSpiConfig));
|
||||
}
|
||||
|
||||
String pathFromJbossDataPath = System.getProperty(JBOSS_SERVER_DATA_DIR) + "/" + PASSWORD_BLACKLISTS_FOLDER;
|
||||
if (!Files.exists(Paths.get(pathFromJbossDataPath))) {
|
||||
if (!Paths.get(pathFromJbossDataPath).toFile().mkdirs()) {
|
||||
LOG.errorf("Could not create folder for password blacklists: %s", pathFromJbossDataPath);
|
||||
}
|
||||
}
|
||||
return ensureExists(Paths.get(pathFromJbossDataPath));
|
||||
}
|
||||
|
||||
private static Path ensureExists(Path path) {
|
||||
|
||||
Objects.requireNonNull(path, "path");
|
||||
|
||||
if (Files.exists(path)) {
|
||||
return path;
|
||||
}
|
||||
|
||||
throw new IllegalStateException("Password blacklists location does not exist: " + path);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -26,3 +26,4 @@ org.keycloak.policy.NotUsernamePasswordPolicyProviderFactory
|
|||
org.keycloak.policy.RegexPatternsPasswordPolicyProviderFactory
|
||||
org.keycloak.policy.SpecialCharsPasswordPolicyProviderFactory
|
||||
org.keycloak.policy.UpperCasePasswordPolicyProviderFactory
|
||||
org.keycloak.policy.BlacklistPasswordPolicyProviderFactory
|
||||
|
|
|
@ -56,6 +56,7 @@
|
|||
<migration.70.version>1.9.8.Final</migration.70.version>
|
||||
<migration.70.authz.version>2.2.1.Final</migration.70.authz.version>
|
||||
<migration.71.version>2.5.5.Final</migration.71.version>
|
||||
<google.guava.version>23.0</google.guava.version>
|
||||
|
||||
<maven.compiler.target>1.8</maven.compiler.target>
|
||||
<maven.compiler.source>1.8</maven.compiler.source>
|
||||
|
|
|
@ -29,6 +29,12 @@
|
|||
<name>Auth Server - Undertow</name>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-testsuite-utils</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jboss.arquillian.junit</groupId>
|
||||
<artifactId>arquillian-junit-container</artifactId>
|
||||
|
|
|
@ -41,23 +41,21 @@ import org.jboss.shrinkwrap.api.spec.WebArchive;
|
|||
import org.jboss.shrinkwrap.descriptor.api.Descriptor;
|
||||
import org.jboss.shrinkwrap.undertow.api.UndertowWebArchive;
|
||||
import org.keycloak.common.util.reflections.Reflections;
|
||||
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.services.filters.KeycloakSessionServletFilter;
|
||||
import org.keycloak.services.managers.ApplianceBootstrap;
|
||||
import org.keycloak.services.resources.KeycloakApplication;
|
||||
|
||||
import org.keycloak.testsuite.KeycloakServer;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.DispatcherType;
|
||||
import javax.servlet.ServletException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class KeycloakOnUndertow implements DeployableContainer<KeycloakOnUndertowConfiguration> {
|
||||
|
||||
|
@ -171,6 +169,8 @@ public class KeycloakOnUndertow implements DeployableContainer<KeycloakOnUnderto
|
|||
return;
|
||||
}
|
||||
|
||||
KeycloakServer.configureDataDirectory();
|
||||
|
||||
log.infof("Starting auth server on embedded Undertow on: http://%s:%d", configuration.getBindAddress(), configuration.getBindHttpPort());
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
|
|
|
@ -20,11 +20,11 @@ package org.keycloak.testsuite.policy;
|
|||
import org.jboss.arquillian.container.test.api.Deployment;
|
||||
import org.jboss.shrinkwrap.api.spec.WebArchive;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.PasswordPolicy;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.policy.BlacklistPasswordPolicyProvider;
|
||||
import org.keycloak.policy.PasswordPolicyManagerProvider;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.testsuite.AbstractKeycloakTest;
|
||||
|
@ -32,7 +32,6 @@ import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
|
|||
import org.keycloak.testsuite.util.RealmBuilder;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.regex.PatternSyntaxException;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
@ -142,6 +141,25 @@ public class PasswordPolicyTest extends AbstractKeycloakTest {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* KEYCLOAK-5244
|
||||
*/
|
||||
@Test
|
||||
public void testBlacklistPasswordPolicyWithTestBlacklist() throws Exception {
|
||||
|
||||
testingClient.server("passwordPolicy").run(session -> {
|
||||
|
||||
RealmModel realmModel = session.getContext().getRealm();
|
||||
PasswordPolicyManagerProvider policyManager = session.getProvider(PasswordPolicyManagerProvider.class);
|
||||
|
||||
realmModel.setPasswordPolicy(PasswordPolicy.parse(session, "passwordBlacklist(test-password-blacklist.txt)"));
|
||||
|
||||
Assert.assertEquals(BlacklistPasswordPolicyProvider.ERROR_MESSAGE, policyManager.validate("jdoe", "blacklisted1").getMessage());
|
||||
Assert.assertEquals(BlacklistPasswordPolicyProvider.ERROR_MESSAGE, policyManager.validate("jdoe", "blacklisted2").getMessage());
|
||||
assertNull(policyManager.validate("jdoe", "notblacklisted"));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotUsername() {
|
||||
testingClient.server("passwordPolicy").run(session -> {
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
blacklisted1
|
||||
blacklisted2
|
|
@ -314,6 +314,9 @@
|
|||
<keycloak.testsuite.logging.pattern>${keycloak.testsuite.logging.pattern}</keycloak.testsuite.logging.pattern>
|
||||
|
||||
<keycloak.connectionsJpa.url.crossdc>${keycloak.connectionsJpa.url.crossdc}</keycloak.connectionsJpa.url.crossdc>
|
||||
|
||||
<!-- used by PasswordPolicyTest.testBlacklistPasswordPolicyWithTestBlacklist, see KEYCLOAK-5244 -->
|
||||
<keycloak.password.blacklists.path>${project.build.directory}/test-classes/password-blacklists</keycloak.password.blacklists.path>
|
||||
</systemPropertyVariables>
|
||||
<properties>
|
||||
<property>
|
||||
|
|
|
@ -40,17 +40,18 @@ import org.keycloak.services.resources.KeycloakApplication;
|
|||
import org.keycloak.testsuite.util.cli.TestsuiteCLI;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.servlet.DispatcherType;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import javax.net.ssl.SSLContext;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
|
@ -58,6 +59,7 @@ import javax.net.ssl.SSLContext;
|
|||
public class KeycloakServer {
|
||||
|
||||
private static final Logger log = Logger.getLogger(KeycloakServer.class);
|
||||
public static final String JBOSS_SERVER_DATA_DIR = "jboss.server.data.dir";
|
||||
|
||||
private boolean sysout = false;
|
||||
|
||||
|
@ -211,6 +213,8 @@ public class KeycloakServer {
|
|||
config.setWorkerThreads(undertowWorkerThreads);
|
||||
}
|
||||
|
||||
configureDataDirectory();
|
||||
|
||||
detectNodeName(config);
|
||||
|
||||
final KeycloakServer keycloak = new KeycloakServer(config);
|
||||
|
@ -241,6 +245,52 @@ public class KeycloakServer {
|
|||
return keycloak;
|
||||
}
|
||||
|
||||
public static void configureDataDirectory() {
|
||||
String dataPath = detectDataDirectory();
|
||||
System.setProperty(JBOSS_SERVER_DATA_DIR, dataPath);
|
||||
log.infof("Using %s %s", JBOSS_SERVER_DATA_DIR, dataPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects the {@code jboss.server.data.dir} to use.
|
||||
* If the System property {@code jboss.server.data.dir} is already set then the property value is used,
|
||||
* otherwise a temporary data dir is created that will be deleted on JVM exit.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String detectDataDirectory() {
|
||||
|
||||
String dataPath = System.getProperty(JBOSS_SERVER_DATA_DIR);
|
||||
|
||||
if (dataPath != null){
|
||||
// we assume jboss.server.data.dir is managed externally so just use it as is.
|
||||
File dataDir = new File(dataPath);
|
||||
if (!dataDir.exists() || !dataDir.isDirectory()) {
|
||||
throw new RuntimeException("Invalid " + JBOSS_SERVER_DATA_DIR + " resources directory: " + dataPath);
|
||||
}
|
||||
|
||||
return dataPath;
|
||||
}
|
||||
|
||||
// we generate a dynamic jboss.server.data.dir and remove it at the end.
|
||||
try {
|
||||
File tempKeycloakFolder = Files.createTempDirectory("keycloak-server-").toFile();
|
||||
File tmpDataDir = new File(tempKeycloakFolder, "/data");
|
||||
|
||||
if (tmpDataDir.mkdirs()) {
|
||||
tmpDataDir.deleteOnExit();
|
||||
} else {
|
||||
throw new IOException("Could not create directory " + tmpDataDir);
|
||||
}
|
||||
|
||||
dataPath = tmpDataDir.getAbsolutePath();
|
||||
} catch (IOException ioe){
|
||||
throw new RuntimeException("Could not create temporary " + JBOSS_SERVER_DATA_DIR, ioe);
|
||||
}
|
||||
|
||||
return dataPath;
|
||||
}
|
||||
|
||||
private KeycloakServerConfig config;
|
||||
|
||||
private KeycloakSessionFactory sessionFactory;
|
||||
|
|
|
@ -110,6 +110,7 @@ invalidPasswordExistingMessage=Das aktuelle Passwort is ung\u00FCltig.
|
|||
invalidPasswordConfirmMessage=Die Passwortbest\u00E4tigung ist nicht identisch.
|
||||
invalidTotpMessage=Ung\u00FCltiger One-time Code.
|
||||
invalidEmailMessage=Ung\u00FCltige E-Mail Adresse.
|
||||
invalidPasswordBlacklistedMessage=Passwort ist nicht erlaubt.
|
||||
|
||||
usernameExistsMessage=Der Benutzername existiert bereits.
|
||||
emailExistsMessage=Die E-Mail-Adresse existiert bereits.
|
||||
|
|
|
@ -152,6 +152,7 @@ invalidPasswordMinSpecialCharsMessage=Invalid password: must contain at least {0
|
|||
invalidPasswordNotUsernameMessage=Invalid password: must not be equal to the username.
|
||||
invalidPasswordRegexPatternMessage=Invalid password: fails to match regex pattern(s).
|
||||
invalidPasswordHistoryMessage=Invalid password: must not be equal to any of last {0} passwords.
|
||||
invalidPasswordBlacklistedMessage=Invalid password: password is blacklisted.
|
||||
invalidPasswordGenericMessage=Invalid password: new password doesn''t match password policies.
|
||||
|
||||
locale_ca=Catal\u00E0
|
||||
|
|
|
@ -6,6 +6,7 @@ invalidPasswordMinSpecialCharsMessage=Invalid password: must contain at least {0
|
|||
invalidPasswordNotUsernameMessage=Invalid password: must not be equal to the username.
|
||||
invalidPasswordRegexPatternMessage=Invalid password: fails to match regex pattern(s).
|
||||
invalidPasswordHistoryMessage=Invalid password: must not be equal to any of last {0} passwords.
|
||||
invalidPasswordBlacklistedMessage=Invalid password: password is blacklisted.
|
||||
invalidPasswordGenericMessage=Invalid password: new password doesn''t match password policies.
|
||||
|
||||
ldapErrorInvalidCustomFilter=Custom configured LDAP filter does not start with "(" or does not end with ")".
|
||||
|
|
|
@ -142,6 +142,7 @@ missingTotpMessage=Please specify authenticator code.
|
|||
notMatchPasswordMessage=Passwords don''t match.
|
||||
|
||||
invalidPasswordExistingMessage=Invalid existing password.
|
||||
invalidPasswordBlacklistedMessage=Invalid password: password is blacklisted.
|
||||
invalidPasswordConfirmMessage=Password confirmation doesn''t match.
|
||||
invalidTotpMessage=Invalid authenticator code.
|
||||
|
||||
|
|
Loading…
Reference in a new issue