From a121f77ea468d07275212668e85a2be7601b6ea0 Mon Sep 17 00:00:00 2001 From: Jan Lieskovsky Date: Fri, 24 Apr 2020 17:21:40 +0200 Subject: [PATCH] [KEYCLOAK-12305] [Testsuite] Check LDAP federated user (in)valid login(s) using various authentication methods, bind credential types, and connection encryption mechanisms The tests cover various possible combinations of the following: * Authentication method: Anonymous or Simple (default), * Bind credential: Secret (default) or Vault, * Connection encryption: Plaintext (default), SSL, or startTLS Also, ignore the StartTLS LDAP tests for now till KEYCLOAK-14343 & KEYCLOAK-14354 are corrected (due these issues they aren't working with auth server Wildfly). They will be re-enabled later via KEYCLOAK-14358 once possible Signed-off-by: Jan Lieskovsky --- .../org/keycloak/testsuite/util/LDAPRule.java | 198 ++++++++++- .../federation/ldap/LDAPUserLoginTest.java | 324 ++++++++++++++++++ .../util/ldap/LDAPEmbeddedServer.java | 102 +++++- 3 files changed, 606 insertions(+), 18 deletions(-) create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPUserLoginTest.java diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/LDAPRule.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/LDAPRule.java index 6dd534d62bc..8e09ab416d2 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/LDAPRule.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/LDAPRule.java @@ -17,12 +17,20 @@ package org.keycloak.testsuite.util; +import org.jboss.logging.Logger; import org.junit.Assume; +import org.junit.runners.model.Statement; +import org.junit.runner.Description; import org.junit.rules.ExternalResource; import org.keycloak.models.LDAPConstants; import org.keycloak.util.ldap.LDAPEmbeddedServer; import java.io.File; +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.util.Map; import java.util.Properties; @@ -33,10 +41,22 @@ import static org.keycloak.testsuite.utils.io.IOUtil.PROJECT_BUILD_DIRECTORY; */ public class LDAPRule extends ExternalResource { + private static final Logger log = Logger.getLogger(LDAPRule.class); + + // Note: Be sure to annotate the testing class with the "EnableVault" annotation + // to get the necessary FilePlaintext vault created automatically for the test + private static final String VAULT_EXPRESSION = "${vault.ldap_bindCredential}"; + public static final String LDAP_CONNECTION_PROPERTIES_LOCATION = "classpath:ldap/ldap-connection.properties"; + private static final String PROPERTY_ENABLE_ACCESS_CONTROL = "enableAccessControl"; + + private static final String PROPERTY_ENABLE_ANONYMOUS_ACCESS = "enableAnonymousAccess"; + private static final String PROPERTY_ENABLE_SSL = "enableSSL"; + private static final String PROPERTY_ENABLE_STARTTLS = "enableStartTLS"; + private static final String PROPERTY_KEYSTORE_FILE = "keystoreFile"; private static final String PRIVATE_KEY = "dependency/keystore/keycloak.jks"; @@ -47,12 +67,13 @@ public class LDAPRule extends ExternalResource { private LDAPEmbeddedServer ldapEmbeddedServer; private LDAPAssume assume; + protected Properties defaultProperties = new Properties(); + public LDAPRule assumeTrue(LDAPAssume assume) { this.assume = assume; return this; } - @Override protected void before() throws Throwable { String connectionPropsLocation = getConnectionPropertiesLocation(); @@ -67,6 +88,80 @@ public class LDAPRule extends ExternalResource { } } + @Override + public Statement apply(Statement base, Description description) { + // Default bind credential value + defaultProperties.setProperty(LDAPConstants.BIND_CREDENTIAL, "secret"); + // Default values of the authentication / access control method and connection encryption to use on the embedded + // LDAP server upon start if not (re)set later via the LDAPConnectionParameters annotation directly on the test + defaultProperties.setProperty(LDAPEmbeddedServer.PROPERTY_ENABLE_ACCESS_CONTROL, "true"); + defaultProperties.setProperty(LDAPEmbeddedServer.PROPERTY_ENABLE_ANONYMOUS_ACCESS, "false"); + defaultProperties.setProperty(LDAPEmbeddedServer.PROPERTY_ENABLE_SSL, "true"); + defaultProperties.setProperty(LDAPEmbeddedServer.PROPERTY_ENABLE_STARTTLS, "false"); + // Default LDAP server confidentiality required value + defaultProperties.setProperty(LDAPEmbeddedServer.PROPERTY_SET_CONFIDENTIALITY_REQUIRED, "false"); + + // Don't auto-update LDAP connection URL read from properties file for LDAP over SSL case even if it's wrong + // (AKA don't try to guess, let the user to get it corrected in the properties file first) + defaultProperties.setProperty("AUTO_UPDATE_LDAP_CONNECTION_URL", "false"); + + Annotation ldapConnectionAnnotation = description.getAnnotation(LDAPConnectionParameters.class); + if (ldapConnectionAnnotation != null) { + // Mark the LDAP connection URL as auto-adjustable to correspond to specific annotation as necessary + defaultProperties.setProperty("AUTO_UPDATE_LDAP_CONNECTION_URL", "true"); + LDAPConnectionParameters connectionParameters = (LDAPConnectionParameters) ldapConnectionAnnotation; + // Configure the bind credential type of the LDAP rule depending on the provided annotation arguments + switch (connectionParameters.bindCredential()) { + case SECRET: + log.debug("Setting bind credential to secret."); + defaultProperties.setProperty(LDAPConstants.BIND_CREDENTIAL, "secret"); + break; + case VAULT: + log.debug("Setting bind credential to vault."); + defaultProperties.setProperty(LDAPConstants.BIND_CREDENTIAL, VAULT_EXPRESSION); + break; + } + // Configure the authentication method of the LDAP rule depending on the provided annotation arguments + switch (connectionParameters.bindType()) { + case NONE: + log.debug("Enabling anonymous authentication method on the LDAP server."); + defaultProperties.setProperty(LDAPEmbeddedServer.PROPERTY_ENABLE_ANONYMOUS_ACCESS, "true"); + defaultProperties.setProperty(LDAPEmbeddedServer.PROPERTY_ENABLE_ACCESS_CONTROL, "false"); + break; + case SIMPLE: + log.debug("Disabling anonymous authentication method on the LDAP server."); + defaultProperties.setProperty(LDAPEmbeddedServer.PROPERTY_ENABLE_ANONYMOUS_ACCESS, "false"); + defaultProperties.setProperty(LDAPEmbeddedServer.PROPERTY_ENABLE_ACCESS_CONTROL, "true"); + break; + } + // Configure the connection encryption of the LDAP rule depending on the provided annotation arguments + switch (connectionParameters.encryption()) { + case NONE: + log.debug("Disabling connection encryption on the LDAP server."); + defaultProperties.setProperty(LDAPEmbeddedServer.PROPERTY_ENABLE_SSL, "false"); + defaultProperties.setProperty(LDAPEmbeddedServer.PROPERTY_ENABLE_STARTTLS, "false"); + break; + case SSL: + log.debug("Enabling SSL connection encryption on the LDAP server."); + defaultProperties.setProperty(LDAPEmbeddedServer.PROPERTY_ENABLE_SSL, "true"); + // Require the LDAP server to accept only secured connections with SSL enabled + log.debug("Configuring the LDAP server to accepts only requests with a secured connection."); + defaultProperties.setProperty(LDAPEmbeddedServer.PROPERTY_SET_CONFIDENTIALITY_REQUIRED, "true"); + defaultProperties.setProperty(LDAPEmbeddedServer.PROPERTY_ENABLE_STARTTLS, "false"); + break; + case STARTTLS: + log.debug("Enabling StartTLS connection encryption on the LDAP server."); + defaultProperties.setProperty(LDAPEmbeddedServer.PROPERTY_ENABLE_STARTTLS, "true"); + // Require the LDAP server to accept only secured connections with StartTLS enabled + log.debug("Configuring the LDAP server to accepts only requests with a secured connection."); + defaultProperties.setProperty(LDAPEmbeddedServer.PROPERTY_SET_CONFIDENTIALITY_REQUIRED, "true"); + defaultProperties.setProperty(LDAPEmbeddedServer.PROPERTY_ENABLE_SSL, "false"); + break; + } + } + return super.apply(base, description); + } + @Override protected void after() { try { @@ -85,10 +180,8 @@ public class LDAPRule extends ExternalResource { } protected LDAPEmbeddedServer createServer() { - Properties defaultProperties = new Properties(); defaultProperties.setProperty(LDAPEmbeddedServer.PROPERTY_DSF, LDAPEmbeddedServer.DSF_INMEMORY); defaultProperties.setProperty(LDAPEmbeddedServer.PROPERTY_LDIF_FILE, "classpath:ldap/users.ldif"); - defaultProperties.setProperty(PROPERTY_ENABLE_SSL, "true"); defaultProperties.setProperty(PROPERTY_CERTIFICATE_PASSWORD, "secret"); defaultProperties.setProperty(PROPERTY_KEYSTORE_FILE, new File(PROJECT_BUILD_DIRECTORY, PRIVATE_KEY).getAbsolutePath()); @@ -96,18 +189,113 @@ public class LDAPRule extends ExternalResource { } public Map getConfig() { - return ldapTestConfiguration.getLDAPConfig(); + Map config = ldapTestConfiguration.getLDAPConfig(); + String ldapConnectionUrl = config.get(LDAPConstants.CONNECTION_URL); + if (ldapConnectionUrl != null && defaultProperties.getProperty("AUTO_UPDATE_LDAP_CONNECTION_URL").equals("true")) { + if ( + ldapConnectionUrl.startsWith("ldap://") && + defaultProperties.getProperty(LDAPEmbeddedServer.PROPERTY_ENABLE_SSL).equals("true") + ) + { + // Switch protocol prefix to "ldaps://" in connection URL if LDAP over SSL is requested + String updatedUrl = ldapConnectionUrl.replaceAll("ldap://", "ldaps://"); + // Flip port number from LDAP to LDAPS + updatedUrl = updatedUrl.replaceAll( + String.valueOf(ldapEmbeddedServer.getBindPort()), + String.valueOf(ldapEmbeddedServer.getBindLdapsPort()) + ); + config.put(LDAPConstants.CONNECTION_URL, updatedUrl); + log.debugf("Using LDAP over SSL \"%s\" connection URL form over: \"%s\" since SSL connection was requested.", updatedUrl, ldapConnectionUrl); + } + if ( + ldapConnectionUrl.startsWith("ldaps://") && + !defaultProperties.getProperty(LDAPEmbeddedServer.PROPERTY_ENABLE_SSL).equals("true") + ) + { + // Switch protocol prefix back to "ldap://" in connection URL if LDAP over SSL flag is not set + String updatedUrl = ldapConnectionUrl.replaceAll("ldaps://", "ldap://"); + // Flip port number from LDAPS to LDAP + updatedUrl = updatedUrl.replaceAll( + String.valueOf(ldapEmbeddedServer.getBindLdapsPort()), + String.valueOf(ldapEmbeddedServer.getBindPort()) + ); + config.put(LDAPConstants.CONNECTION_URL, updatedUrl); + log.debugf("Using plaintext / startTLS \"%s\" connection URL form over: \"%s\" since plaintext / startTLS connection was requested.", updatedUrl, ldapConnectionUrl); + } + } + switch (defaultProperties.getProperty(LDAPConstants.BIND_CREDENTIAL)) { + case VAULT_EXPRESSION: + config.put(LDAPConstants.BIND_CREDENTIAL, VAULT_EXPRESSION); + break; + default: + // Default to secret as the bind credential + config.put(LDAPConstants.BIND_CREDENTIAL, "secret"); + } + switch (defaultProperties.getProperty(LDAPEmbeddedServer.PROPERTY_ENABLE_ANONYMOUS_ACCESS)) { + case "true": + config.put(LDAPConstants.AUTH_TYPE, LDAPConstants.AUTH_TYPE_NONE); + break; + default: + // Default to username + password LDAP authentication method + config.put(LDAPConstants.AUTH_TYPE, LDAPConstants.AUTH_TYPE_SIMPLE); + } + switch (defaultProperties.getProperty(LDAPEmbeddedServer.PROPERTY_ENABLE_STARTTLS)) { + case "true": + config.put(LDAPConstants.START_TLS, "true"); + break; + default: + // Default to startTLS disabled + config.put(LDAPConstants.START_TLS, "false"); + } + switch (defaultProperties.getProperty(LDAPEmbeddedServer.PROPERTY_SET_CONFIDENTIALITY_REQUIRED)) { + case "true": + System.setProperty("PROPERTY_SET_CONFIDENTIALITY_REQUIRED", "true"); + break; + default: + // Configure the LDAP server to accept not secured connections from clients by default + System.setProperty("PROPERTY_SET_CONFIDENTIALITY_REQUIRED", "false"); + } + return config; } public int getSleepTime() { return ldapTestConfiguration.getSleepTime(); } - /** Allows to run particular LDAP test just under specific conditions (eg. some test running just on Active Directory) **/ public interface LDAPAssume { boolean assumeTrue(LDAPTestConfiguration ldapConfig); } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + public @interface LDAPConnectionParameters { + // Default to secret as the bind credential unless annotated otherwise + BindCredential bindCredential() default LDAPConnectionParameters.BindCredential.SECRET; + // Disable anonymous LDAP authentication by default unless annotated otherwise + BindType bindType() default LDAPConnectionParameters.BindType.SIMPLE; + // Enable SSL encrypted LDAP connections (along with the unencrypted ones) by default unless annotated otherwise + Encryption encryption() default LDAPConnectionParameters.Encryption.SSL; + + public enum BindCredential { + SECRET, + VAULT + } + + public enum BindType { + NONE, + SIMPLE + } + + public enum Encryption { + NONE, + // Important: Choosing either of "SSL" or "STARTTLS" connection encryption methods below + // will also configure the LDAP server to accept only a secured connection from clients + // (IOW plaintext client connections will be prohibited). Use those two options with care! + SSL, + STARTTLS + } + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPUserLoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPUserLoginTest.java new file mode 100644 index 00000000000..fc49c9ea8ac --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPUserLoginTest.java @@ -0,0 +1,324 @@ +/* + * Copyright 2020 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.testsuite.federation.ldap; + +import org.jboss.arquillian.graphene.page.Page; +import org.junit.FixMethodOrder; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.junit.runners.MethodSorters; +import org.keycloak.events.Errors; +import org.keycloak.events.EventType; +import org.keycloak.models.LDAPConstants; +import org.keycloak.models.RealmModel; +import org.keycloak.OAuth2Constants; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.storage.ldap.idm.model.LDAPObject; +import org.keycloak.testsuite.arquillian.annotation.EnableVault; +import org.keycloak.testsuite.Assert; +import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.pages.AppPage; +import org.keycloak.testsuite.pages.LoginPage; +import org.keycloak.testsuite.util.LDAPRule; +import org.keycloak.testsuite.util.LDAPRule.LDAPConnectionParameters; +import org.keycloak.testsuite.util.LDAPTestConfiguration; +import org.keycloak.testsuite.util.LDAPTestUtils; + +import java.util.HashMap; +import java.util.Map; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Test user logins utilizing various LDAP authentication methods and different LDAP connection encryption mechanisms. + * + * @author Jan Lieskovsky + */ +@EnableVault +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class LDAPUserLoginTest extends AbstractLDAPTest { + + @Rule + // Start an embedded LDAP server with configuration derived from test annotations before each test + public LDAPRule ldapRule = new LDAPRule() + .assumeTrue((LDAPTestConfiguration ldapConfig) -> { + + return ldapConfig.isStartEmbeddedLdapServer(); + + }); + + @Override + protected LDAPRule getLDAPRule() { + return ldapRule; + } + + @Rule + // Recreate a new LDAP provider based on test annotations before each test + public ExternalResource ldapProviderRule = new ExternalResource() { + + @Override + protected void after() { + // Delete the previously imported realm(s) after each test. This forces + // a new LDAP provider with custom configuration (derived from the test + // annotations) to be created each time the next test is run + if (getTestingClient() != null) { + getTestContext().getTestRealmReps().clear(); + } + } + + }; + + @Rule + public AssertEvents events = new AssertEvents(this); + + protected static final Map DEFAULT_TEST_USERS = new HashMap(); + static { + DEFAULT_TEST_USERS.put("EMPTY_USER_PASSWORD", new String()); + DEFAULT_TEST_USERS.put("INVALID_USER_NAME", "userUnknown"); + DEFAULT_TEST_USERS.put("INVALID_USER_EMAIL", "unknown@keycloak.org"); + DEFAULT_TEST_USERS.put("INVALID_USER_PASSWORD", "1nval!D"); + DEFAULT_TEST_USERS.put("VALID_USER_EMAIL", "jdoe@keycloak.org"); + DEFAULT_TEST_USERS.put("VALID_USER_NAME", "jdoe"); + DEFAULT_TEST_USERS.put("VALID_USER_FIRST_NAME", "John"); + DEFAULT_TEST_USERS.put("VALID_USER_LAST_NAME", "Doe"); + DEFAULT_TEST_USERS.put("VALID_USER_PASSWORD", "P@ssw0rd!"); + DEFAULT_TEST_USERS.put("VALID_USER_POSTAL_CODE", "12345"); + DEFAULT_TEST_USERS.put("VALID_USER_STREET", "1th Avenue"); + } + + @Override + protected void afterImportTestRealm() { + getTestingClient().server().run(session -> { + LDAPTestContext ctx = LDAPTestContext.init(session); + RealmModel appRealm = ctx.getRealm(); + + // Delete all LDAP users + LDAPTestUtils.removeAllLDAPUsers(ctx.getLdapProvider(), appRealm); + // Add some new LDAP users for testing + LDAPObject john = LDAPTestUtils.addLDAPUser + ( + ctx.getLdapProvider(), + appRealm, + DEFAULT_TEST_USERS.get("VALID_USER_NAME"), + DEFAULT_TEST_USERS.get("VALID_USER_FIRST_NAME"), + DEFAULT_TEST_USERS.get("VALID_USER_LAST_NAME"), + DEFAULT_TEST_USERS.get("VALID_USER_EMAIL"), + DEFAULT_TEST_USERS.get("VALID_USER_STREET"), + DEFAULT_TEST_USERS.get("VALID_USER_POSTAL_CODE") + ); + LDAPTestUtils.updateLDAPPassword(ctx.getLdapProvider(), john, DEFAULT_TEST_USERS.get("VALID_USER_PASSWORD")); + }); + } + + @Page + protected AppPage appPage; + + @Page + protected LoginPage loginPage; + + // Helper methods + private void verifyLoginSucceededAndLogout(String username, String password) { + loginPage.open(); + loginPage.login(username, password); + appPage.assertCurrent(); + Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); + Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); + appPage.logout(); + } + + private void verifyLoginFailed(String username, String password) { + // Clear the events queue before the actual test to catch all errors properly + events.clear(); + // Run the test actions + loginPage.open(); + loginPage.login(username, password); + Assert.assertEquals("Invalid username or password.", loginPage.getError()); + + if (username.equals(DEFAULT_TEST_USERS.get("INVALID_USER_EMAIL")) || username.equals(DEFAULT_TEST_USERS.get("INVALID_USER_NAME"))) { + + events.expect(EventType.LOGIN_ERROR).user((String) null).error(Errors.USER_NOT_FOUND).assertEvent(); + + } else if (username.equals(DEFAULT_TEST_USERS.get("VALID_USER_EMAIL")) || username.equals(DEFAULT_TEST_USERS.get("VALID_USER_NAME"))) { + + List knownUsers = getAdminClient().realm(TEST_REALM_NAME).users().search(DEFAULT_TEST_USERS.get("VALID_USER_NAME")); + Assert.assertTrue(!knownUsers.isEmpty()); + final String userId = knownUsers.get(0).getId(); + events.expect(EventType.LOGIN_ERROR).user(userId).error(Errors.INVALID_USER_CREDENTIALS).assertEvent(); + + } + } + + private void runLDAPLoginTest() { + final String emptyPassword = DEFAULT_TEST_USERS.get("EMPTY_USER_PASSWORD"); + final String invalidEmail = DEFAULT_TEST_USERS.get("INVALID_USER_EMAIL"); + final String invalidPassword = DEFAULT_TEST_USERS.get("INVALID_USER_PASSWORD"); + final String invalidUsername = DEFAULT_TEST_USERS.get("INVALID_USER_NAME"); + final String validEmail = DEFAULT_TEST_USERS.get("VALID_USER_EMAIL"); + final String validPassword = DEFAULT_TEST_USERS.get("VALID_USER_PASSWORD"); + final String validUsername = DEFAULT_TEST_USERS.get("VALID_USER_NAME"); + + // Check LDAP login via valid username + valid password + verifyLoginSucceededAndLogout(validUsername, validPassword); + // Check LDAP login via valid email + valid password + verifyLoginSucceededAndLogout(validEmail, validPassword); + // Check LDAP login via valid username + empty password + verifyLoginFailed(validUsername, emptyPassword); + // Check LDAP login via valid email + empty password + verifyLoginFailed(validEmail, emptyPassword); + // Check LDAP login via valid username + invalid password + verifyLoginFailed(validUsername, invalidPassword); + // Check LDAP login via valid email + invalid password + verifyLoginFailed(validEmail, invalidPassword); + // Check LDAP login via invalid username + verifyLoginFailed(invalidUsername, invalidPassword); + // Check LDAP login via invalid email + verifyLoginFailed(invalidEmail, invalidPassword); + } + + private void verifyConnectionUrlProtocolPrefix(String ldapProtocolPrefix) { + final String ldapConnectionUrl = ldapRule.getConfig().get(LDAPConstants.CONNECTION_URL); + Assert.assertTrue(!ldapConnectionUrl.isEmpty() && ldapConnectionUrl.startsWith(ldapProtocolPrefix)); + } + + // Tests themselves + + // Check LDAP federated user (in)valid login(s) with simple authentication & encryption (both SSL and startTLS) disabled + // Test variant: Bind credential set to secret (default) + @Test + @LDAPConnectionParameters(bindType=LDAPConnectionParameters.BindType.SIMPLE, encryption=LDAPConnectionParameters.Encryption.NONE) + public void loginLDAPUserAuthenticationSimpleEncryptionNone() { + verifyConnectionUrlProtocolPrefix("ldap://"); + runLDAPLoginTest(); + } + + // Check LDAP federated user (in)valid login(s) with simple authentication & encryption (both SSL and startTLS) disabled + // Test variant: Bind credential set to vault + @Test + @LDAPConnectionParameters(bindCredential=LDAPConnectionParameters.BindCredential.VAULT, bindType=LDAPConnectionParameters.BindType.SIMPLE, encryption=LDAPConnectionParameters.Encryption.NONE) + public void loginLDAPUserCredentialVaultAuthenticationSimpleEncryptionNone() { + verifyConnectionUrlProtocolPrefix("ldap://"); + runLDAPLoginTest(); + } + + // Check LDAP federated user (in)valid login(s) with simple authentication & SSL encryption enabled + // Test variant: Bind credential set to secret (default) + @Test + @LDAPConnectionParameters(bindType=LDAPConnectionParameters.BindType.SIMPLE, encryption=LDAPConnectionParameters.Encryption.SSL) + public void loginLDAPUserAuthenticationSimpleEncryptionSSL() { + verifyConnectionUrlProtocolPrefix("ldaps://"); + runLDAPLoginTest(); + } + + // Check LDAP federated user (in)valid login(s) with simple authentication & SSL encryption enabled + // Test variant: Bind credential set to vault + @Test + @LDAPConnectionParameters(bindCredential=LDAPConnectionParameters.BindCredential.VAULT, bindType=LDAPConnectionParameters.BindType.SIMPLE, encryption=LDAPConnectionParameters.Encryption.SSL) + public void loginLDAPUserCredentialVaultAuthenticationSimpleEncryptionSSL() { + verifyConnectionUrlProtocolPrefix("ldaps://"); + runLDAPLoginTest(); + } + + // Check LDAP federated user (in)valid login(s) with simple authentication & startTLS encryption enabled + // Test variant: Bind credential set to secret (default) + // KEYCLOAK-14358 - Disable the StartTLS LDAP tests till KEYCLOAK-14343 & KEYCLOAK-14354 are corrected + // since they don't work properly with auth server Wildfly due these bugs + @Ignore + @Test + @LDAPConnectionParameters(bindType=LDAPConnectionParameters.BindType.SIMPLE, encryption=LDAPConnectionParameters.Encryption.STARTTLS) + public void loginLDAPUserAuthenticationSimpleEncryptionStartTLS() { + verifyConnectionUrlProtocolPrefix("ldap://"); + runLDAPLoginTest(); + } + + // Check LDAP federated user (in)valid login(s) with simple authentication & startTLS encryption enabled + // Test variant: Bind credential set to vault + // KEYCLOAK-14358 - Disable the StartTLS LDAP tests till KEYCLOAK-14343 & KEYCLOAK-14354 are corrected + // since they don't work properly with auth server Wildfly due these bugs + @Ignore + @Test + @LDAPConnectionParameters(bindCredential=LDAPConnectionParameters.BindCredential.VAULT, bindType=LDAPConnectionParameters.BindType.SIMPLE, encryption=LDAPConnectionParameters.Encryption.STARTTLS) + public void loginLDAPUserCredentialVaultAuthenticationSimpleEncryptionStartTLS() { + verifyConnectionUrlProtocolPrefix("ldap://"); + runLDAPLoginTest(); + } + + // Check LDAP federated user (in)valid login(s) with anonymous authentication & encryption (both SSL and startTLS) disabled + // Test variant: Bind credential set to secret (default) + @Test + @LDAPConnectionParameters(bindType=LDAPConnectionParameters.BindType.NONE, encryption=LDAPConnectionParameters.Encryption.NONE) + public void loginLDAPUserAuthenticationNoneEncryptionNone() { + verifyConnectionUrlProtocolPrefix("ldap://"); + runLDAPLoginTest(); + } + + // Check LDAP federated user (in)valid login(s) with anonymous authentication & encryption (both SSL and startTLS) disabled + // Test variant: Bind credential set to vault + @Test + @LDAPConnectionParameters(bindCredential=LDAPConnectionParameters.BindCredential.VAULT, bindType=LDAPConnectionParameters.BindType.NONE, encryption=LDAPConnectionParameters.Encryption.NONE) + public void loginLDAPUserCredentialVaultAuthenticationNoneEncryptionNone() { + verifyConnectionUrlProtocolPrefix("ldap://"); + runLDAPLoginTest(); + } + + // Check LDAP federated user (in)valid login(s) with anonymous authentication & SSL encryption enabled + // Test variant: Bind credential set to secret (default) + @Test + @LDAPConnectionParameters(bindType=LDAPConnectionParameters.BindType.NONE, encryption=LDAPConnectionParameters.Encryption.SSL) + public void loginLDAPUserAuthenticationNoneEncryptionSSL() { + verifyConnectionUrlProtocolPrefix("ldaps://"); + runLDAPLoginTest(); + } + + // Check LDAP federated user (in)valid login(s) with anonymous authentication & SSL encryption enabled + // Test variant: Bind credential set to vault + @Test + @LDAPConnectionParameters(bindCredential=LDAPConnectionParameters.BindCredential.VAULT, bindType=LDAPConnectionParameters.BindType.NONE, encryption=LDAPConnectionParameters.Encryption.SSL) + public void loginLDAPUserCredentialVaultAuthenticationNoneEncryptionSSL() { + verifyConnectionUrlProtocolPrefix("ldaps://"); + runLDAPLoginTest(); + } + + // Check LDAP federated user (in)valid login(s) with anonymous authentication & startTLS encryption enabled + // Test variant: Bind credential set to secret (default) + // KEYCLOAK-14358 - Disable the StartTLS LDAP tests till KEYCLOAK-14343 & KEYCLOAK-14354 are corrected + // since they don't work properly with auth server Wildfly due these bugs + @Ignore + @Test + @LDAPConnectionParameters(bindType=LDAPConnectionParameters.BindType.NONE, encryption=LDAPConnectionParameters.Encryption.STARTTLS) + public void loginLDAPUserAuthenticationNoneEncryptionStartTLS() { + verifyConnectionUrlProtocolPrefix("ldap://"); + runLDAPLoginTest(); + } + + // Check LDAP federated user (in)valid login(s) with anonymous authentication & startTLS encryption enabled + // Test variant: Bind credential set to vault + // KEYCLOAK-14358 - Disable the StartTLS LDAP tests till KEYCLOAK-14343 & KEYCLOAK-14354 are corrected + // since they don't work properly with auth server Wildfly due these bugs + @Ignore + @Test + @LDAPConnectionParameters(bindCredential=LDAPConnectionParameters.BindCredential.VAULT, bindType=LDAPConnectionParameters.BindType.NONE, encryption=LDAPConnectionParameters.Encryption.STARTTLS) + public void loginLDAPUserCredentialVaultAuthenticationNoneEncryptionStartTLS() { + verifyConnectionUrlProtocolPrefix("ldap://"); + runLDAPLoginTest(); + } +} diff --git a/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/LDAPEmbeddedServer.java b/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/LDAPEmbeddedServer.java index ed6d25fe960..79fc1d6f8b5 100644 --- a/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/LDAPEmbeddedServer.java +++ b/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/LDAPEmbeddedServer.java @@ -33,6 +33,8 @@ import org.apache.directory.server.core.factory.AvlPartitionFactory; import org.apache.directory.server.core.factory.DefaultDirectoryServiceFactory; import org.apache.directory.server.core.factory.JdbmPartitionFactory; import org.apache.directory.server.core.normalization.NormalizationInterceptor; +import org.apache.directory.server.ldap.ExtendedOperationHandler; +import org.apache.directory.server.ldap.handlers.extended.StartTlsHandler; import org.apache.directory.server.ldap.LdapServer; import org.apache.directory.server.ldap.handlers.extended.PwdModifyHandler; import org.apache.directory.server.protocol.shared.transport.TcpTransport; @@ -63,13 +65,17 @@ public class LDAPEmbeddedServer { public static final String PROPERTY_LDIF_FILE = "ldap.ldif"; public static final String PROPERTY_SASL_PRINCIPAL = "ldap.saslPrincipal"; public static final String PROPERTY_DSF = "ldap.dsf"; + public static final String PROPERTY_ENABLE_ACCESS_CONTROL = "enableAccessControl"; + public static final String PROPERTY_ENABLE_ANONYMOUS_ACCESS = "enableAnonymousAccess"; + public static final String PROPERTY_ENABLE_SSL = "enableSSL"; + public static final String PROPERTY_ENABLE_STARTTLS = "enableStartTLS"; + public static final String PROPERTY_SET_CONFIDENTIALITY_REQUIRED = "setConfidentialityRequired"; private static final String DEFAULT_BASE_DN = "dc=keycloak,dc=org"; private static final String DEFAULT_BIND_HOST = "localhost"; private static final String DEFAULT_BIND_PORT = "10389"; private static final String DEFAULT_BIND_LDAPS_PORT = "10636"; private static final String DEFAULT_LDIF_FILE = "classpath:ldap/default-users.ldif"; - private static final String PROPERTY_ENABLE_SSL = "enableSSL"; private static final String PROPERTY_KEYSTORE_FILE = "keystoreFile"; private static final String PROPERTY_CERTIFICATE_PASSWORD = "certificatePassword"; @@ -86,13 +92,24 @@ public class LDAPEmbeddedServer { protected String ldifFile; protected String ldapSaslPrincipal; protected String directoryServiceFactory; + protected boolean enableAccessControl = false; + protected boolean enableAnonymousAccess = false; protected boolean enableSSL = false; + protected boolean enableStartTLS = false; + protected boolean setConfidentialityRequired = false; protected String keystoreFile; protected String certPassword; protected DirectoryService directoryService; protected LdapServer ldapServer; + public int getBindPort() { + return bindPort; + } + + public int getBindLdapsPort() { + return bindLdapsPort; + } public static void main(String[] args) throws Exception { Properties defaultProperties = new Properties(); @@ -132,7 +149,11 @@ public class LDAPEmbeddedServer { this.ldifFile = readProperty(PROPERTY_LDIF_FILE, DEFAULT_LDIF_FILE); this.ldapSaslPrincipal = readProperty(PROPERTY_SASL_PRINCIPAL, null); this.directoryServiceFactory = readProperty(PROPERTY_DSF, DEFAULT_DSF); + this.enableAccessControl = Boolean.valueOf(readProperty(PROPERTY_ENABLE_ACCESS_CONTROL, "false")); + this.enableAnonymousAccess = Boolean.valueOf(readProperty(PROPERTY_ENABLE_ANONYMOUS_ACCESS, "false")); this.enableSSL = Boolean.valueOf(readProperty(PROPERTY_ENABLE_SSL, "false")); + this.enableStartTLS = Boolean.valueOf(readProperty(PROPERTY_ENABLE_STARTTLS, "false")); + this.setConfidentialityRequired = Boolean.valueOf(readProperty(PROPERTY_SET_CONFIDENTIALITY_REQUIRED, "false")); this.keystoreFile = readProperty(PROPERTY_KEYSTORE_FILE, null); this.certPassword = readProperty(PROPERTY_CERTIFICATE_PASSWORD, null); } @@ -161,15 +182,22 @@ public class LDAPEmbeddedServer { log.info("Importing LDIF: " + ldifFile); importLdif(); - log.info("Creating LDAP Server"); + log.info("Creating LDAP server.."); this.ldapServer = createLdapServer(); } public void start() throws Exception { - log.info("Starting LDAP Server"); + log.info("Starting LDAP server.."); ldapServer.start(); - log.info("LDAP Server started"); + // Verify the server started properly + if (ldapServer.isStarted() && ldapServer.getDirectoryService().isStarted()) { + log.info("LDAP server started."); + } else if(!ldapServer.isStarted()) { + throw new RuntimeException("Failed to start the LDAP server!"); + } else if (!ldapServer.getDirectoryService().isStarted()) { + throw new RuntimeException("Failed to start the directory service for the LDAP server!"); + } } @@ -188,8 +216,8 @@ public class LDAPEmbeddedServer { DefaultDirectoryServiceFactory dsf = new DefaultDirectoryServiceFactory(); DirectoryService service = dsf.getDirectoryService(); - service.setAccessControlEnabled(false); - service.setAllowAnonymousAccess(false); + service.setAccessControlEnabled(enableAccessControl); + service.setAllowAnonymousAccess(enableAnonymousAccess); service.getChangeLog().setEnabled(false); dsf.init(dcName + "DS"); @@ -242,16 +270,44 @@ public class LDAPEmbeddedServer { ldapServer.setServiceName("DefaultLdapServer"); ldapServer.setSearchBaseDn(this.baseDN); + // Tolerate plaintext LDAP connections from clients by default + ldapServer.setConfidentialityRequired(this.setConfidentialityRequired); // Read the transports Transport ldap = new TcpTransport(this.bindHost, this.bindPort, 3, 50); ldapServer.addTransports( ldap ); - if (enableSSL) { - Transport ldaps = new TcpTransport(this.bindHost, this.bindLdapsPort, 3, 50); - ldaps.setEnableSSL(true); + if (enableSSL || enableStartTLS) { ldapServer.setKeystoreFile(keystoreFile); ldapServer.setCertificatePassword(certPassword); - ldapServer.addTransports( ldaps ); + if (enableSSL) { + Transport ldaps = new TcpTransport(this.bindHost, this.bindLdapsPort, 3, 50); + ldaps.setEnableSSL(true); + ldapServer.addTransports( ldaps ); + if (ldaps.isSSLEnabled()) { + log.info("Enabled SSL support on the LDAP server."); + } + } + if (enableStartTLS) { + try { + ldapServer.addExtendedOperationHandler(new StartTlsHandler()); + } catch (Exception e) { + throw new IllegalStateException("Cannot add the StartTLS extension handler: ", e); + } + for (ExtendedOperationHandler eoh : ldapServer.getExtendedOperationHandlers()) { + if (eoh.getOid().equals(StartTlsHandler.EXTENSION_OID)) { + log.info("Enabled StartTLS support on the LDAP server."); + break; + } + } + } + } + + // Require the LDAP server to accept only encrypted connections if confidentiality requested + if (setConfidentialityRequired) { + ldapServer.setConfidentialityRequired(true); + if (ldapServer.isConfidentialityRequired()) { + log.info("Configured the LDAP server to accepts only requests with a secured connection."); + } } // Associate the DS to this LdapServer @@ -264,8 +320,28 @@ public class LDAPEmbeddedServer { throw new IllegalStateException("It wasn't possible to add PwdModifyHandler"); } - // Propagate the anonymous flag to the DS - directoryService.setAllowAnonymousAccess(false); + if (enableAccessControl) { + if (enableAnonymousAccess) { + throw new IllegalStateException("Illegal to enable both the access control subsystem and the anonymous access at the same time! See: http://directory.apache.org/apacheds/gen-docs/latest/apidocs/src-html/org/apache/directory/server/core/DefaultDirectoryService.html#line.399 for details."); + } else { + directoryService.setAccessControlEnabled(true); + if (directoryService.isAccessControlEnabled()) { + log.info("Enabled basic access control checks on the LDAP server."); + } + } + } else { + if (enableAnonymousAccess) { + directoryService.setAllowAnonymousAccess(true); + // Since per ApacheDS JavaDoc: http://directory.apache.org/apacheds/gen-docs/latest/apidocs/src-html/org/apache/directory/server/core/DefaultDirectoryService.html#line.399 + // "if the access control subsystem is enabled then access to some entries may not + // be allowed even when full anonymous access is enabled", disable the access control + // subsystem together with enabling anonymous access to prevent this + directoryService.setAccessControlEnabled(false); + if (directoryService.isAllowAnonymousAccess() && !directoryService.isAccessControlEnabled()) { + log.info("Enabled anonymous access on the LDAP server."); + } + } + } return ldapServer; } @@ -299,7 +375,7 @@ public class LDAPEmbeddedServer { try { directoryService.getAdminSession().add(new DefaultEntry(directoryService.getSchemaManager(), ldifEntry.getEntry())); } catch (LdapEntryAlreadyExistsException ignore) { - log.info("Entry " + ldifEntry.getDn() + " already exists. Ignoring"); + log.info("Entry " + ldifEntry.getDn() + " already exists. Ignoring."); } } } finally {