[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 <jlieskov@redhat.com>
This commit is contained in:
Jan Lieskovsky 2020-04-24 17:21:40 +02:00 committed by Marek Posolda
parent e8dc10b4a1
commit a121f77ea4
3 changed files with 606 additions and 18 deletions

View file

@ -17,12 +17,20 @@
package org.keycloak.testsuite.util; package org.keycloak.testsuite.util;
import org.jboss.logging.Logger;
import org.junit.Assume; import org.junit.Assume;
import org.junit.runners.model.Statement;
import org.junit.runner.Description;
import org.junit.rules.ExternalResource; import org.junit.rules.ExternalResource;
import org.keycloak.models.LDAPConstants; import org.keycloak.models.LDAPConstants;
import org.keycloak.util.ldap.LDAPEmbeddedServer; import org.keycloak.util.ldap.LDAPEmbeddedServer;
import java.io.File; 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.Map;
import java.util.Properties; import java.util.Properties;
@ -33,10 +41,22 @@ import static org.keycloak.testsuite.utils.io.IOUtil.PROJECT_BUILD_DIRECTORY;
*/ */
public class LDAPRule extends ExternalResource { 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"; 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_SSL = "enableSSL";
private static final String PROPERTY_ENABLE_STARTTLS = "enableStartTLS";
private static final String PROPERTY_KEYSTORE_FILE = "keystoreFile"; private static final String PROPERTY_KEYSTORE_FILE = "keystoreFile";
private static final String PRIVATE_KEY = "dependency/keystore/keycloak.jks"; private static final String PRIVATE_KEY = "dependency/keystore/keycloak.jks";
@ -47,12 +67,13 @@ public class LDAPRule extends ExternalResource {
private LDAPEmbeddedServer ldapEmbeddedServer; private LDAPEmbeddedServer ldapEmbeddedServer;
private LDAPAssume assume; private LDAPAssume assume;
protected Properties defaultProperties = new Properties();
public LDAPRule assumeTrue(LDAPAssume assume) { public LDAPRule assumeTrue(LDAPAssume assume) {
this.assume = assume; this.assume = assume;
return this; return this;
} }
@Override @Override
protected void before() throws Throwable { protected void before() throws Throwable {
String connectionPropsLocation = getConnectionPropertiesLocation(); 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 @Override
protected void after() { protected void after() {
try { try {
@ -85,10 +180,8 @@ public class LDAPRule extends ExternalResource {
} }
protected LDAPEmbeddedServer createServer() { protected LDAPEmbeddedServer createServer() {
Properties defaultProperties = new Properties();
defaultProperties.setProperty(LDAPEmbeddedServer.PROPERTY_DSF, LDAPEmbeddedServer.DSF_INMEMORY); defaultProperties.setProperty(LDAPEmbeddedServer.PROPERTY_DSF, LDAPEmbeddedServer.DSF_INMEMORY);
defaultProperties.setProperty(LDAPEmbeddedServer.PROPERTY_LDIF_FILE, "classpath:ldap/users.ldif"); 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_CERTIFICATE_PASSWORD, "secret");
defaultProperties.setProperty(PROPERTY_KEYSTORE_FILE, new File(PROJECT_BUILD_DIRECTORY, PRIVATE_KEY).getAbsolutePath()); defaultProperties.setProperty(PROPERTY_KEYSTORE_FILE, new File(PROJECT_BUILD_DIRECTORY, PRIVATE_KEY).getAbsolutePath());
@ -96,18 +189,113 @@ public class LDAPRule extends ExternalResource {
} }
public Map<String, String> getConfig() { public Map<String, String> getConfig() {
return ldapTestConfiguration.getLDAPConfig(); Map<String, String> 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() { public int getSleepTime() {
return ldapTestConfiguration.getSleepTime(); return ldapTestConfiguration.getSleepTime();
} }
/** Allows to run particular LDAP test just under specific conditions (eg. some test running just on Active Directory) **/ /** Allows to run particular LDAP test just under specific conditions (eg. some test running just on Active Directory) **/
public interface LDAPAssume { public interface LDAPAssume {
boolean assumeTrue(LDAPTestConfiguration ldapConfig); 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
}
}
} }

View file

@ -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 <a href="mailto:jlieskov@redhat.com">Jan Lieskovsky</a>
*/
@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<String, String> DEFAULT_TEST_USERS = new HashMap<String, String>();
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<UserRepresentation> 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();
}
}

View file

@ -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.DefaultDirectoryServiceFactory;
import org.apache.directory.server.core.factory.JdbmPartitionFactory; import org.apache.directory.server.core.factory.JdbmPartitionFactory;
import org.apache.directory.server.core.normalization.NormalizationInterceptor; 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.LdapServer;
import org.apache.directory.server.ldap.handlers.extended.PwdModifyHandler; import org.apache.directory.server.ldap.handlers.extended.PwdModifyHandler;
import org.apache.directory.server.protocol.shared.transport.TcpTransport; 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_LDIF_FILE = "ldap.ldif";
public static final String PROPERTY_SASL_PRINCIPAL = "ldap.saslPrincipal"; public static final String PROPERTY_SASL_PRINCIPAL = "ldap.saslPrincipal";
public static final String PROPERTY_DSF = "ldap.dsf"; 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_BASE_DN = "dc=keycloak,dc=org";
private static final String DEFAULT_BIND_HOST = "localhost"; private static final String DEFAULT_BIND_HOST = "localhost";
private static final String DEFAULT_BIND_PORT = "10389"; private static final String DEFAULT_BIND_PORT = "10389";
private static final String DEFAULT_BIND_LDAPS_PORT = "10636"; 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 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_KEYSTORE_FILE = "keystoreFile";
private static final String PROPERTY_CERTIFICATE_PASSWORD = "certificatePassword"; private static final String PROPERTY_CERTIFICATE_PASSWORD = "certificatePassword";
@ -86,13 +92,24 @@ public class LDAPEmbeddedServer {
protected String ldifFile; protected String ldifFile;
protected String ldapSaslPrincipal; protected String ldapSaslPrincipal;
protected String directoryServiceFactory; protected String directoryServiceFactory;
protected boolean enableAccessControl = false;
protected boolean enableAnonymousAccess = false;
protected boolean enableSSL = false; protected boolean enableSSL = false;
protected boolean enableStartTLS = false;
protected boolean setConfidentialityRequired = false;
protected String keystoreFile; protected String keystoreFile;
protected String certPassword; protected String certPassword;
protected DirectoryService directoryService; protected DirectoryService directoryService;
protected LdapServer ldapServer; protected LdapServer ldapServer;
public int getBindPort() {
return bindPort;
}
public int getBindLdapsPort() {
return bindLdapsPort;
}
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {
Properties defaultProperties = new Properties(); Properties defaultProperties = new Properties();
@ -132,7 +149,11 @@ public class LDAPEmbeddedServer {
this.ldifFile = readProperty(PROPERTY_LDIF_FILE, DEFAULT_LDIF_FILE); this.ldifFile = readProperty(PROPERTY_LDIF_FILE, DEFAULT_LDIF_FILE);
this.ldapSaslPrincipal = readProperty(PROPERTY_SASL_PRINCIPAL, null); this.ldapSaslPrincipal = readProperty(PROPERTY_SASL_PRINCIPAL, null);
this.directoryServiceFactory = readProperty(PROPERTY_DSF, DEFAULT_DSF); 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.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.keystoreFile = readProperty(PROPERTY_KEYSTORE_FILE, null);
this.certPassword = readProperty(PROPERTY_CERTIFICATE_PASSWORD, null); this.certPassword = readProperty(PROPERTY_CERTIFICATE_PASSWORD, null);
} }
@ -161,15 +182,22 @@ public class LDAPEmbeddedServer {
log.info("Importing LDIF: " + ldifFile); log.info("Importing LDIF: " + ldifFile);
importLdif(); importLdif();
log.info("Creating LDAP Server"); log.info("Creating LDAP server..");
this.ldapServer = createLdapServer(); this.ldapServer = createLdapServer();
} }
public void start() throws Exception { public void start() throws Exception {
log.info("Starting LDAP Server"); log.info("Starting LDAP server..");
ldapServer.start(); 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(); DefaultDirectoryServiceFactory dsf = new DefaultDirectoryServiceFactory();
DirectoryService service = dsf.getDirectoryService(); DirectoryService service = dsf.getDirectoryService();
service.setAccessControlEnabled(false); service.setAccessControlEnabled(enableAccessControl);
service.setAllowAnonymousAccess(false); service.setAllowAnonymousAccess(enableAnonymousAccess);
service.getChangeLog().setEnabled(false); service.getChangeLog().setEnabled(false);
dsf.init(dcName + "DS"); dsf.init(dcName + "DS");
@ -242,16 +270,44 @@ public class LDAPEmbeddedServer {
ldapServer.setServiceName("DefaultLdapServer"); ldapServer.setServiceName("DefaultLdapServer");
ldapServer.setSearchBaseDn(this.baseDN); ldapServer.setSearchBaseDn(this.baseDN);
// Tolerate plaintext LDAP connections from clients by default
ldapServer.setConfidentialityRequired(this.setConfidentialityRequired);
// Read the transports // Read the transports
Transport ldap = new TcpTransport(this.bindHost, this.bindPort, 3, 50); Transport ldap = new TcpTransport(this.bindHost, this.bindPort, 3, 50);
ldapServer.addTransports( ldap ); ldapServer.addTransports( ldap );
if (enableSSL) { if (enableSSL || enableStartTLS) {
Transport ldaps = new TcpTransport(this.bindHost, this.bindLdapsPort, 3, 50);
ldaps.setEnableSSL(true);
ldapServer.setKeystoreFile(keystoreFile); ldapServer.setKeystoreFile(keystoreFile);
ldapServer.setCertificatePassword(certPassword); 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 // Associate the DS to this LdapServer
@ -264,8 +320,28 @@ public class LDAPEmbeddedServer {
throw new IllegalStateException("It wasn't possible to add PwdModifyHandler"); throw new IllegalStateException("It wasn't possible to add PwdModifyHandler");
} }
// Propagate the anonymous flag to the DS if (enableAccessControl) {
directoryService.setAllowAnonymousAccess(false); 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; return ldapServer;
} }
@ -299,7 +375,7 @@ public class LDAPEmbeddedServer {
try { try {
directoryService.getAdminSession().add(new DefaultEntry(directoryService.getSchemaManager(), ldifEntry.getEntry())); directoryService.getAdminSession().add(new DefaultEntry(directoryService.getSchemaManager(), ldifEntry.getEntry()));
} catch (LdapEntryAlreadyExistsException ignore) { } catch (LdapEntryAlreadyExistsException ignore) {
log.info("Entry " + ldifEntry.getDn() + " already exists. Ignoring"); log.info("Entry " + ldifEntry.getDn() + " already exists. Ignoring.");
} }
} }
} finally { } finally {