[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.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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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":
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
Loading…
Reference in a new issue