[KEYCLOAK-14343] Truststore SPI support for LDAP with StartTLS

Signed-off-by:  Tero Saarni <tero.saarni@est.tech>
Co-authored-by: Jan Lieskovsky <jlieskov@redhat.com>
This commit is contained in:
Tero Saarni 2020-05-29 11:11:14 +03:00 committed by Marek Posolda
parent e16f30d31f
commit 3c82f523ff
8 changed files with 52 additions and 14 deletions

View file

@ -4,6 +4,7 @@ import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.LDAPConstants; import org.keycloak.models.LDAPConstants;
import org.keycloak.storage.ldap.LDAPConfig; import org.keycloak.storage.ldap.LDAPConfig;
import org.keycloak.truststore.TruststoreProvider;
import org.keycloak.vault.VaultCharSecret; import org.keycloak.vault.VaultCharSecret;
import javax.naming.AuthenticationException; import javax.naming.AuthenticationException;
@ -13,6 +14,8 @@ import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext; import javax.naming.ldap.LdapContext;
import javax.naming.ldap.StartTlsRequest; import javax.naming.ldap.StartTlsRequest;
import javax.naming.ldap.StartTlsResponse; import javax.naming.ldap.StartTlsResponse;
import javax.net.ssl.SSLSocketFactory;
import java.io.IOException; import java.io.IOException;
import java.nio.CharBuffer; import java.nio.CharBuffer;
import java.util.HashMap; import java.util.HashMap;
@ -76,15 +79,21 @@ public final class LDAPContextManager implements AutoCloseable {
ldapContext = new InitialLdapContext(connProp, null); ldapContext = new InitialLdapContext(connProp, null);
if (ldapConfig.isStartTls()) { if (ldapConfig.isStartTls()) {
SSLSocketFactory sslSocketFactory = null;
String useTruststoreSpi = ldapConfig.getUseTruststoreSpi();
if (useTruststoreSpi != null && useTruststoreSpi.equals(LDAPConstants.USE_TRUSTSTORE_ALWAYS)) {
TruststoreProvider provider = session.getProvider(TruststoreProvider.class);
sslSocketFactory = provider.getSSLSocketFactory();
}
tlsResponse = startTLS(ldapContext, ldapConfig.getAuthType(), ldapConfig.getBindDN(), tlsResponse = startTLS(ldapContext, ldapConfig.getAuthType(), ldapConfig.getBindDN(),
vaultCharSecret.getAsArray().orElse(ldapConfig.getBindCredential().toCharArray())); vaultCharSecret.getAsArray().orElse(ldapConfig.getBindCredential().toCharArray()), sslSocketFactory);
// Exception should be already thrown by LDAPContextManager.startTLS if "startTLS" could not be established, but rather do some additional check // Exception should be already thrown by LDAPContextManager.startTLS if "startTLS" could not be established, but rather do some additional check
if (tlsResponse == null) { if (tlsResponse == null) {
throw new NamingException("Wasn't able to establish LDAP connection through StartTLS"); throw new NamingException("Wasn't able to establish LDAP connection through StartTLS");
} }
} }
} }
public LdapContext getLdapContext() throws NamingException { public LdapContext getLdapContext() throws NamingException {
@ -99,12 +108,12 @@ public final class LDAPContextManager implements AutoCloseable {
: session.vault().getCharSecret(ldapConfig.getBindCredential()); : session.vault().getCharSecret(ldapConfig.getBindCredential());
} }
public static StartTlsResponse startTLS(LdapContext ldapContext, String authType, String bindDN, char[] bindCredential) throws NamingException { public static StartTlsResponse startTLS(LdapContext ldapContext, String authType, String bindDN, char[] bindCredential, SSLSocketFactory sslSocketFactory) throws NamingException {
StartTlsResponse tls = null; StartTlsResponse tls = null;
try { try {
tls = (StartTlsResponse) ldapContext.extendedOperation(new StartTlsRequest()); tls = (StartTlsResponse) ldapContext.extendedOperation(new StartTlsRequest());
tls.negotiate(); tls.negotiate(sslSocketFactory);
ldapContext.addToEnvironment(Context.SECURITY_AUTHENTICATION, authType); ldapContext.addToEnvironment(Context.SECURITY_AUTHENTICATION, authType);
@ -179,8 +188,12 @@ public final class LDAPContextManager implements AutoCloseable {
logger.warn("LDAP URL is null. LDAPOperationManager won't work correctly"); logger.warn("LDAP URL is null. LDAPOperationManager won't work correctly");
} }
String useTruststoreSpi = ldapConfig.getUseTruststoreSpi(); // when using Start TLS, use default socket factory for LDAP client but pass the TrustStore SSL socket factory later
LDAPConstants.setTruststoreSpiIfNeeded(useTruststoreSpi, url, env); // when calling StartTlsResponse.negotiate(trustStoreSSLSocketFactory)
if (!ldapConfig.isStartTls()) {
String useTruststoreSpi = ldapConfig.getUseTruststoreSpi();
LDAPConstants.setTruststoreSpiIfNeeded(useTruststoreSpi, url, env);
}
String connectionPooling = ldapConfig.getConnectionPooling(); String connectionPooling = ldapConfig.getConnectionPooling();
if (connectionPooling != null) { if (connectionPooling != null) {

View file

@ -27,6 +27,7 @@ import org.keycloak.storage.ldap.idm.model.LDAPDn;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery; import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.storage.ldap.idm.store.ldap.extended.PasswordModifyRequest; import org.keycloak.storage.ldap.idm.store.ldap.extended.PasswordModifyRequest;
import org.keycloak.storage.ldap.mappers.LDAPOperationDecorator; import org.keycloak.storage.ldap.mappers.LDAPOperationDecorator;
import org.keycloak.truststore.TruststoreProvider;
import javax.naming.AuthenticationException; import javax.naming.AuthenticationException;
import javax.naming.Binding; import javax.naming.Binding;
@ -47,6 +48,8 @@ import javax.naming.ldap.LdapName;
import javax.naming.ldap.PagedResultsControl; import javax.naming.ldap.PagedResultsControl;
import javax.naming.ldap.PagedResultsResponseControl; import javax.naming.ldap.PagedResultsResponseControl;
import javax.naming.ldap.StartTlsResponse; import javax.naming.ldap.StartTlsResponse;
import javax.net.ssl.SSLSocketFactory;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
@ -511,7 +514,14 @@ public class LDAPOperationManager {
authCtx = new InitialLdapContext(env, null); authCtx = new InitialLdapContext(env, null);
if (config.isStartTls()) { if (config.isStartTls()) {
tlsResponse = LDAPContextManager.startTLS(authCtx, "simple", dn, password.toCharArray()); SSLSocketFactory sslSocketFactory = null;
String useTruststoreSpi = config.getUseTruststoreSpi();
if (useTruststoreSpi != null && useTruststoreSpi.equals(LDAPConstants.USE_TRUSTSTORE_ALWAYS)) {
TruststoreProvider provider = session.getProvider(TruststoreProvider.class);
sslSocketFactory = provider.getSSLSocketFactory();
}
tlsResponse = LDAPContextManager.startTLS(authCtx, "simple", dn, password.toCharArray(), sslSocketFactory);
// Exception should be already thrown by LDAPContextManager.startTLS if "startTLS" could not be established, but rather do some additional check // Exception should be already thrown by LDAPContextManager.startTLS if "startTLS" could not be established, but rather do some additional check
if (tlsResponse == null) { if (tlsResponse == null) {

View file

@ -19,10 +19,10 @@ package org.keycloak.truststore;
import org.keycloak.provider.Provider; import org.keycloak.provider.Provider;
import java.security.KeyStore;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.security.KeyStore;
import java.util.Map; import java.util.Map;
import javax.net.ssl.SSLSocketFactory;
import javax.security.auth.x500.X500Principal; import javax.security.auth.x500.X500Principal;
/** /**
@ -32,6 +32,8 @@ public interface TruststoreProvider extends Provider {
HostnameVerificationPolicy getPolicy(); HostnameVerificationPolicy getPolicy();
SSLSocketFactory getSSLSocketFactory();
KeyStore getTruststore(); KeyStore getTruststore();
/** /**

View file

@ -20,7 +20,7 @@ package org.keycloak.truststore;
import java.security.KeyStore; import java.security.KeyStore;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.Map; import java.util.Map;
import javax.net.ssl.SSLSocketFactory;
import javax.security.auth.x500.X500Principal; import javax.security.auth.x500.X500Principal;
/** /**
@ -29,6 +29,7 @@ import javax.security.auth.x500.X500Principal;
public class FileTruststoreProvider implements TruststoreProvider { public class FileTruststoreProvider implements TruststoreProvider {
private final HostnameVerificationPolicy policy; private final HostnameVerificationPolicy policy;
private final SSLSocketFactory sslSocketFactory;
private final KeyStore truststore; private final KeyStore truststore;
private final Map<X500Principal, X509Certificate> rootCertificates; private final Map<X500Principal, X509Certificate> rootCertificates;
private final Map<X500Principal, X509Certificate> intermediateCertificates; private final Map<X500Principal, X509Certificate> intermediateCertificates;
@ -38,6 +39,9 @@ public class FileTruststoreProvider implements TruststoreProvider {
this.truststore = truststore; this.truststore = truststore;
this.rootCertificates = rootCertificates; this.rootCertificates = rootCertificates;
this.intermediateCertificates = intermediateCertificates; this.intermediateCertificates = intermediateCertificates;
SSLSocketFactory jsseSSLSocketFactory = new JSSETruststoreConfigurator(this).getSSLSocketFactory();
this.sslSocketFactory = (jsseSSLSocketFactory != null) ? jsseSSLSocketFactory : (SSLSocketFactory) javax.net.ssl.SSLSocketFactory.getDefault();
} }
@Override @Override
@ -45,6 +49,11 @@ public class FileTruststoreProvider implements TruststoreProvider {
return policy; return policy;
} }
@Override
public SSLSocketFactory getSSLSocketFactory() {
return sslSocketFactory;
}
@Override @Override
public KeyStore getTruststore() { public KeyStore getTruststore() {
return truststore; return truststore;

View file

@ -22,7 +22,6 @@ import org.keycloak.Config;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import javax.security.auth.x500.X500Principal;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
@ -40,6 +39,7 @@ import java.security.cert.X509Certificate;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import javax.security.auth.x500.X500Principal;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>

View file

@ -242,10 +242,14 @@ public class LDAPRule extends ExternalResource {
switch (defaultProperties.getProperty(LDAPEmbeddedServer.PROPERTY_ENABLE_STARTTLS)) { switch (defaultProperties.getProperty(LDAPEmbeddedServer.PROPERTY_ENABLE_STARTTLS)) {
case "true": case "true":
config.put(LDAPConstants.START_TLS, "true"); config.put(LDAPConstants.START_TLS, "true");
// Use truststore from TruststoreSPI also for StartTLS connections
config.put(LDAPConstants.USE_TRUSTSTORE_SPI, LDAPConstants.USE_TRUSTSTORE_ALWAYS);
break; break;
default: default:
// Default to startTLS disabled // Default to startTLS disabled
config.put(LDAPConstants.START_TLS, "false"); config.put(LDAPConstants.START_TLS, "false");
// By default use truststore from TruststoreSPI only for LDAP over SSL connections
config.put(LDAPConstants.USE_TRUSTSTORE_SPI, LDAPConstants.USE_TRUSTSTORE_LDAPS_ONLY);
} }
switch (defaultProperties.getProperty(LDAPEmbeddedServer.PROPERTY_SET_CONFIDENTIALITY_REQUIRED)) { switch (defaultProperties.getProperty(LDAPEmbeddedServer.PROPERTY_SET_CONFIDENTIALITY_REQUIRED)) {
case "true": case "true":

View file

@ -72,6 +72,7 @@ public class LDAPTestConfiguration {
PROP_MAPPINGS.put(KerberosConstants.ALLOW_PASSWORD_AUTHENTICATION, "idm.test.kerberos.allow.password.authentication"); PROP_MAPPINGS.put(KerberosConstants.ALLOW_PASSWORD_AUTHENTICATION, "idm.test.kerberos.allow.password.authentication");
PROP_MAPPINGS.put(KerberosConstants.UPDATE_PROFILE_FIRST_LOGIN, "idm.test.kerberos.update.profile.first.login"); PROP_MAPPINGS.put(KerberosConstants.UPDATE_PROFILE_FIRST_LOGIN, "idm.test.kerberos.update.profile.first.login");
PROP_MAPPINGS.put(KerberosConstants.USE_KERBEROS_FOR_PASSWORD_AUTHENTICATION, "idm.test.kerberos.use.kerberos.for.password.authentication"); PROP_MAPPINGS.put(KerberosConstants.USE_KERBEROS_FOR_PASSWORD_AUTHENTICATION, "idm.test.kerberos.use.kerberos.for.password.authentication");
PROP_MAPPINGS.put(LDAPConstants.USE_TRUSTSTORE_SPI, "idm.test.ldap.truststore.spi");
DEFAULT_VALUES.put(LDAPConstants.CONNECTION_URL, "ldap://localhost:10389"); DEFAULT_VALUES.put(LDAPConstants.CONNECTION_URL, "ldap://localhost:10389");
DEFAULT_VALUES.put(LDAPConstants.BASE_DN, "dc=keycloak,dc=org"); DEFAULT_VALUES.put(LDAPConstants.BASE_DN, "dc=keycloak,dc=org");
@ -85,6 +86,7 @@ public class LDAPTestConfiguration {
DEFAULT_VALUES.put(LDAPConstants.USERNAME_LDAP_ATTRIBUTE, null); DEFAULT_VALUES.put(LDAPConstants.USERNAME_LDAP_ATTRIBUTE, null);
DEFAULT_VALUES.put(LDAPConstants.USER_OBJECT_CLASSES, null); DEFAULT_VALUES.put(LDAPConstants.USER_OBJECT_CLASSES, null);
DEFAULT_VALUES.put(LDAPConstants.EDIT_MODE, UserStorageProvider.EditMode.READ_ONLY.toString()); DEFAULT_VALUES.put(LDAPConstants.EDIT_MODE, UserStorageProvider.EditMode.READ_ONLY.toString());
DEFAULT_VALUES.put(LDAPConstants.USE_TRUSTSTORE_SPI, LDAPConstants.USE_TRUSTSTORE_ALWAYS);
DEFAULT_VALUES.put(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION, "false"); DEFAULT_VALUES.put(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION, "false");
DEFAULT_VALUES.put(KerberosConstants.KERBEROS_REALM, "KEYCLOAK.ORG"); DEFAULT_VALUES.put(KerberosConstants.KERBEROS_REALM, "KEYCLOAK.ORG");
@ -102,7 +104,7 @@ public class LDAPTestConfiguration {
ldapTestConfiguration.loadConnectionProperties(connectionPropertiesLocation); ldapTestConfiguration.loadConnectionProperties(connectionPropertiesLocation);
return ldapTestConfiguration; return ldapTestConfiguration;
} }
public static String getResource(String resourcePath) { public static String getResource(String resourcePath) {
URL urlPath = LDAPTestConfiguration.class.getResource(resourcePath); URL urlPath = LDAPTestConfiguration.class.getResource(resourcePath);
String absolutePath = new File(urlPath.getFile()).getAbsolutePath(); String absolutePath = new File(urlPath.getFile()).getAbsolutePath();

View file

@ -242,7 +242,6 @@ public class LDAPUserLoginTest extends AbstractLDAPTest {
// Test variant: Bind credential set to secret (default) // Test variant: Bind credential set to secret (default)
// KEYCLOAK-14358 - Disable the StartTLS LDAP tests till KEYCLOAK-14343 & KEYCLOAK-14354 are corrected // 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 // since they don't work properly with auth server Wildfly due these bugs
@Ignore
@Test @Test
@LDAPConnectionParameters(bindType=LDAPConnectionParameters.BindType.SIMPLE, encryption=LDAPConnectionParameters.Encryption.STARTTLS) @LDAPConnectionParameters(bindType=LDAPConnectionParameters.BindType.SIMPLE, encryption=LDAPConnectionParameters.Encryption.STARTTLS)
public void loginLDAPUserAuthenticationSimpleEncryptionStartTLS() { public void loginLDAPUserAuthenticationSimpleEncryptionStartTLS() {
@ -254,7 +253,6 @@ public class LDAPUserLoginTest extends AbstractLDAPTest {
// Test variant: Bind credential set to vault // Test variant: Bind credential set to vault
// KEYCLOAK-14358 - Disable the StartTLS LDAP tests till KEYCLOAK-14343 & KEYCLOAK-14354 are corrected // 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 // since they don't work properly with auth server Wildfly due these bugs
@Ignore
@Test @Test
@LDAPConnectionParameters(bindCredential=LDAPConnectionParameters.BindCredential.VAULT, bindType=LDAPConnectionParameters.BindType.SIMPLE, encryption=LDAPConnectionParameters.Encryption.STARTTLS) @LDAPConnectionParameters(bindCredential=LDAPConnectionParameters.BindCredential.VAULT, bindType=LDAPConnectionParameters.BindType.SIMPLE, encryption=LDAPConnectionParameters.Encryption.STARTTLS)
public void loginLDAPUserCredentialVaultAuthenticationSimpleEncryptionStartTLS() { public void loginLDAPUserCredentialVaultAuthenticationSimpleEncryptionStartTLS() {