[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:
parent
e16f30d31f
commit
3c82f523ff
8 changed files with 52 additions and 14 deletions
|
@ -4,6 +4,7 @@ import org.jboss.logging.Logger;
|
|||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.storage.ldap.LDAPConfig;
|
||||
import org.keycloak.truststore.TruststoreProvider;
|
||||
import org.keycloak.vault.VaultCharSecret;
|
||||
|
||||
import javax.naming.AuthenticationException;
|
||||
|
@ -13,6 +14,8 @@ import javax.naming.ldap.InitialLdapContext;
|
|||
import javax.naming.ldap.LdapContext;
|
||||
import javax.naming.ldap.StartTlsRequest;
|
||||
import javax.naming.ldap.StartTlsResponse;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.CharBuffer;
|
||||
import java.util.HashMap;
|
||||
|
@ -76,15 +79,21 @@ public final class LDAPContextManager implements AutoCloseable {
|
|||
|
||||
ldapContext = new InitialLdapContext(connProp, null);
|
||||
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(),
|
||||
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
|
||||
if (tlsResponse == null) {
|
||||
throw new NamingException("Wasn't able to establish LDAP connection through StartTLS");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public LdapContext getLdapContext() throws NamingException {
|
||||
|
@ -99,12 +108,12 @@ public final class LDAPContextManager implements AutoCloseable {
|
|||
: 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;
|
||||
|
||||
try {
|
||||
tls = (StartTlsResponse) ldapContext.extendedOperation(new StartTlsRequest());
|
||||
tls.negotiate();
|
||||
tls.negotiate(sslSocketFactory);
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
// when using Start TLS, use default socket factory for LDAP client but pass the TrustStore SSL socket factory later
|
||||
// when calling StartTlsResponse.negotiate(trustStoreSSLSocketFactory)
|
||||
if (!ldapConfig.isStartTls()) {
|
||||
String useTruststoreSpi = ldapConfig.getUseTruststoreSpi();
|
||||
LDAPConstants.setTruststoreSpiIfNeeded(useTruststoreSpi, url, env);
|
||||
}
|
||||
|
||||
String connectionPooling = ldapConfig.getConnectionPooling();
|
||||
if (connectionPooling != null) {
|
||||
|
|
|
@ -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.store.ldap.extended.PasswordModifyRequest;
|
||||
import org.keycloak.storage.ldap.mappers.LDAPOperationDecorator;
|
||||
import org.keycloak.truststore.TruststoreProvider;
|
||||
|
||||
import javax.naming.AuthenticationException;
|
||||
import javax.naming.Binding;
|
||||
|
@ -47,6 +48,8 @@ import javax.naming.ldap.LdapName;
|
|||
import javax.naming.ldap.PagedResultsControl;
|
||||
import javax.naming.ldap.PagedResultsResponseControl;
|
||||
import javax.naming.ldap.StartTlsResponse;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
@ -511,7 +514,14 @@ public class LDAPOperationManager {
|
|||
|
||||
authCtx = new InitialLdapContext(env, null);
|
||||
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
|
||||
if (tlsResponse == null) {
|
||||
|
|
|
@ -19,10 +19,10 @@ package org.keycloak.truststore;
|
|||
|
||||
import org.keycloak.provider.Provider;
|
||||
|
||||
import java.security.KeyStore;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.KeyStore;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.security.auth.x500.X500Principal;
|
||||
|
||||
/**
|
||||
|
@ -32,6 +32,8 @@ public interface TruststoreProvider extends Provider {
|
|||
|
||||
HostnameVerificationPolicy getPolicy();
|
||||
|
||||
SSLSocketFactory getSSLSocketFactory();
|
||||
|
||||
KeyStore getTruststore();
|
||||
|
||||
/**
|
||||
|
|
|
@ -20,7 +20,7 @@ package org.keycloak.truststore;
|
|||
import java.security.KeyStore;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.security.auth.x500.X500Principal;
|
||||
|
||||
/**
|
||||
|
@ -29,6 +29,7 @@ import javax.security.auth.x500.X500Principal;
|
|||
public class FileTruststoreProvider implements TruststoreProvider {
|
||||
|
||||
private final HostnameVerificationPolicy policy;
|
||||
private final SSLSocketFactory sslSocketFactory;
|
||||
private final KeyStore truststore;
|
||||
private final Map<X500Principal, X509Certificate> rootCertificates;
|
||||
private final Map<X500Principal, X509Certificate> intermediateCertificates;
|
||||
|
@ -38,6 +39,9 @@ public class FileTruststoreProvider implements TruststoreProvider {
|
|||
this.truststore = truststore;
|
||||
this.rootCertificates = rootCertificates;
|
||||
this.intermediateCertificates = intermediateCertificates;
|
||||
|
||||
SSLSocketFactory jsseSSLSocketFactory = new JSSETruststoreConfigurator(this).getSSLSocketFactory();
|
||||
this.sslSocketFactory = (jsseSSLSocketFactory != null) ? jsseSSLSocketFactory : (SSLSocketFactory) javax.net.ssl.SSLSocketFactory.getDefault();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -45,6 +49,11 @@ public class FileTruststoreProvider implements TruststoreProvider {
|
|||
return policy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SSLSocketFactory getSSLSocketFactory() {
|
||||
return sslSocketFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyStore getTruststore() {
|
||||
return truststore;
|
||||
|
|
|
@ -22,7 +22,6 @@ import org.keycloak.Config;
|
|||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
|
||||
import javax.security.auth.x500.X500Principal;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
|
@ -40,6 +39,7 @@ import java.security.cert.X509Certificate;
|
|||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import javax.security.auth.x500.X500Principal;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||
|
|
|
@ -242,10 +242,14 @@ public class LDAPRule extends ExternalResource {
|
|||
switch (defaultProperties.getProperty(LDAPEmbeddedServer.PROPERTY_ENABLE_STARTTLS)) {
|
||||
case "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;
|
||||
default:
|
||||
// Default to startTLS disabled
|
||||
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)) {
|
||||
case "true":
|
||||
|
|
|
@ -72,6 +72,7 @@ public class LDAPTestConfiguration {
|
|||
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.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.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.USER_OBJECT_CLASSES, null);
|
||||
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.KERBEROS_REALM, "KEYCLOAK.ORG");
|
||||
|
|
|
@ -242,7 +242,6 @@ public class LDAPUserLoginTest extends AbstractLDAPTest {
|
|||
// 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() {
|
||||
|
@ -254,7 +253,6 @@ public class LDAPUserLoginTest extends AbstractLDAPTest {
|
|||
// 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() {
|
||||
|
|
Loading…
Reference in a new issue