From 3103e0fd0a6803a203f9869bd2f180999da77db9 Mon Sep 17 00:00:00 2001 From: Thomas Darimont Date: Tue, 17 Oct 2017 20:41:44 +0200 Subject: [PATCH] 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 --- .../main/module.xml | 1 + pom.xml | 10 + server-spi-private/pom.xml | 5 + .../BlacklistPasswordPolicyProvider.java | 74 ++++ ...lacklistPasswordPolicyProviderFactory.java | 331 ++++++++++++++++++ ...cloak.policy.PasswordPolicyProviderFactory | 1 + testsuite/integration-arquillian/pom.xml | 3 +- .../servers/auth-server/undertow/pom.xml | 6 + .../undertow/KeycloakOnUndertow.java | 10 +- .../testsuite/policy/PasswordPolicyTest.java | 22 +- .../test-password-blacklist.txt | 2 + .../integration-arquillian/tests/pom.xml | 3 + .../keycloak/testsuite/KeycloakServer.java | 52 ++- .../account/messages/messages_de.properties | 1 + .../account/messages/messages_en.properties | 1 + .../admin/messages/messages_en.properties | 1 + .../login/messages/messages_en.properties | 1 + 17 files changed, 515 insertions(+), 9 deletions(-) create mode 100644 server-spi-private/src/main/java/org/keycloak/policy/BlacklistPasswordPolicyProvider.java create mode 100644 server-spi-private/src/main/java/org/keycloak/policy/BlacklistPasswordPolicyProviderFactory.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/resources/password-blacklists/test-password-blacklist.txt diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-server-spi-private/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-server-spi-private/main/module.xml index be103dde61..978718bcef 100755 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-server-spi-private/main/module.xml +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-server-spi-private/main/module.xml @@ -38,5 +38,6 @@ + diff --git a/pom.xml b/pom.xml index 87b288b674..22846e2274 100755 --- a/pom.xml +++ b/pom.xml @@ -91,6 +91,10 @@ 2.0.0-M21 1.0.0-M33 3.2.1 + + + 20.0 + 2.3.23 9.1.0.v20131115 3.4.1 @@ -440,6 +444,12 @@ ${google.zxing.version} + + com.google.guava + guava + ${google.guava.version} + + com.icegreen diff --git a/server-spi-private/pom.xml b/server-spi-private/pom.xml index 1fb137b6c2..3f9dc4f639 100755 --- a/server-spi-private/pom.xml +++ b/server-spi-private/pom.xml @@ -76,6 +76,11 @@ httpclient provided + + com.google.guava + guava + provided + junit junit diff --git a/server-spi-private/src/main/java/org/keycloak/policy/BlacklistPasswordPolicyProvider.java b/server-spi-private/src/main/java/org/keycloak/policy/BlacklistPasswordPolicyProvider.java new file mode 100644 index 0000000000..f114c24c6c --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/policy/BlacklistPasswordPolicyProvider.java @@ -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 Thomas Darimont + */ +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 + } +} diff --git a/server-spi-private/src/main/java/org/keycloak/policy/BlacklistPasswordPolicyProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/policy/BlacklistPasswordPolicyProviderFactory.java new file mode 100644 index 0000000000..cb7af6253b --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/policy/BlacklistPasswordPolicyProviderFactory.java @@ -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. + *

+ * 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. + *

Blacklists can be configured via the Authentication: Password Policy section in the admin-console. + * A blacklist-file is referred to by its name in the policy configuration. + *

Users can provide custom blacklists by adding a blacklist password file to the configured blacklist folder. + *

+ *

The location of the password-blacklists folder is derived as follows

+ *
    + *
  1. the value of the System property {@code keycloak.password.blacklists.path} if configured - fails if folder is missing
  2. + *
  3. the value of the SPI config property: {@code blacklistsPath} when explicitly configured - fails if folder is missing
  4. + *
  5. otherwise {@code ${jboss.server.data.dir}/password-blacklists/} if nothing else is configured - the folder is created automatically if not present
  6. + *
+ *

Note that the preferred way for configuration is to copy the password file to the {@code ${jboss.server.data.dir}/password-blacklists/} folder

+ *

To configure a password blacklist via the SPI configuration, run the following jboss-cli script:

+ *
{@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/)
+ * }
+ *

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 Authentication: Password Policy configuration. + * + * @author Thomas Darimont + */ +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 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}. + *

+ * 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 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 load() { + + try { + LOG.infof("Loading blacklist with name %s from %s - start", name, path); + + long passwordCount = getPasswordCount(); + + BloomFilter 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. + *

+ *

    + *
  1. + * system property {@code keycloak.password.blacklists.path} if present + *
  2. + *
  3. SPI config property {@code blacklistsPath}
  4. + *
+ * 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); + } + } +} diff --git a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.policy.PasswordPolicyProviderFactory b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.policy.PasswordPolicyProviderFactory index a436fe9799..ac72ac5693 100644 --- a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.policy.PasswordPolicyProviderFactory +++ b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.policy.PasswordPolicyProviderFactory @@ -26,3 +26,4 @@ org.keycloak.policy.NotUsernamePasswordPolicyProviderFactory org.keycloak.policy.RegexPatternsPasswordPolicyProviderFactory org.keycloak.policy.SpecialCharsPasswordPolicyProviderFactory org.keycloak.policy.UpperCasePasswordPolicyProviderFactory +org.keycloak.policy.BlacklistPasswordPolicyProviderFactory diff --git a/testsuite/integration-arquillian/pom.xml b/testsuite/integration-arquillian/pom.xml index c21ea8fb23..9b162e1cd4 100644 --- a/testsuite/integration-arquillian/pom.xml +++ b/testsuite/integration-arquillian/pom.xml @@ -56,7 +56,8 @@ 1.9.8.Final 2.2.1.Final 2.5.5.Final - + 23.0 + 1.8 1.8 diff --git a/testsuite/integration-arquillian/servers/auth-server/undertow/pom.xml b/testsuite/integration-arquillian/servers/auth-server/undertow/pom.xml index 26c82a191c..578b86afd2 100644 --- a/testsuite/integration-arquillian/servers/auth-server/undertow/pom.xml +++ b/testsuite/integration-arquillian/servers/auth-server/undertow/pom.xml @@ -29,6 +29,12 @@ Auth Server - Undertow + + + org.keycloak + keycloak-testsuite-utils + + org.jboss.arquillian.junit arquillian-junit-container diff --git a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertow.java b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertow.java index b77d9e052d..dac8ea73c6 100644 --- a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertow.java +++ b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertow.java @@ -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 { @@ -171,6 +169,8 @@ public class KeycloakOnUndertow implements DeployableContainer { + + 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 -> { diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/password-blacklists/test-password-blacklist.txt b/testsuite/integration-arquillian/tests/base/src/test/resources/password-blacklists/test-password-blacklist.txt new file mode 100644 index 0000000000..922808ca65 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/password-blacklists/test-password-blacklist.txt @@ -0,0 +1,2 @@ +blacklisted1 +blacklisted2 \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/pom.xml b/testsuite/integration-arquillian/tests/pom.xml index f4dcc81dbf..77711590d6 100755 --- a/testsuite/integration-arquillian/tests/pom.xml +++ b/testsuite/integration-arquillian/tests/pom.xml @@ -314,6 +314,9 @@ ${keycloak.testsuite.logging.pattern} ${keycloak.connectionsJpa.url.crossdc} + + + ${project.build.directory}/test-classes/password-blacklists diff --git a/testsuite/utils/src/main/java/org/keycloak/testsuite/KeycloakServer.java b/testsuite/utils/src/main/java/org/keycloak/testsuite/KeycloakServer.java index 0ebc0d8f8c..9ab36b050c 100755 --- a/testsuite/utils/src/main/java/org/keycloak/testsuite/KeycloakServer.java +++ b/testsuite/utils/src/main/java/org/keycloak/testsuite/KeycloakServer.java @@ -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 Stian Thorgersen @@ -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; diff --git a/themes/src/main/resources-community/theme/base/account/messages/messages_de.properties b/themes/src/main/resources-community/theme/base/account/messages/messages_de.properties index f323249160..339132376e 100644 --- a/themes/src/main/resources-community/theme/base/account/messages/messages_de.properties +++ b/themes/src/main/resources-community/theme/base/account/messages/messages_de.properties @@ -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. diff --git a/themes/src/main/resources/theme/base/account/messages/messages_en.properties b/themes/src/main/resources/theme/base/account/messages/messages_en.properties index c5ce32a39b..e98874b43e 100755 --- a/themes/src/main/resources/theme/base/account/messages/messages_en.properties +++ b/themes/src/main/resources/theme/base/account/messages/messages_en.properties @@ -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 diff --git a/themes/src/main/resources/theme/base/admin/messages/messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/messages_en.properties index 82db91e4f2..ea61dc3ee9 100644 --- a/themes/src/main/resources/theme/base/admin/messages/messages_en.properties +++ b/themes/src/main/resources/theme/base/admin/messages/messages_en.properties @@ -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 ")". diff --git a/themes/src/main/resources/theme/base/login/messages/messages_en.properties b/themes/src/main/resources/theme/base/login/messages/messages_en.properties index 9abb4feab4..1277f77b84 100755 --- a/themes/src/main/resources/theme/base/login/messages/messages_en.properties +++ b/themes/src/main/resources/theme/base/login/messages/messages_en.properties @@ -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.