Fix the issue with LDAP connectionUrl containing multiple hosts

Closes #17359
This commit is contained in:
mposolda 2023-03-31 17:02:24 +02:00 committed by Marek Posolda
parent d7c3678096
commit 1cbdf4d17e
12 changed files with 434 additions and 51 deletions

View file

@ -30,6 +30,12 @@
<name>Keycloak Crypto FIPS 140-2 Integration</name> <name>Keycloak Crypto FIPS 140-2 Integration</name>
<description/> <description/>
<properties>
<maven.compiler.release>11</maven.compiler.release>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>

View file

@ -0,0 +1,318 @@
/*
* Copyright 2023 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.crypto.fips;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.SocketAddress;
import java.net.SocketException;
import java.nio.channels.SocketChannel;
import java.util.List;
import java.util.function.BiFunction;
import javax.net.ssl.HandshakeCompletedListener;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
/**
* Forked from wildfly-elytron
*
* @author <a href="mailto:david.lloyd@redhat.com">David M. Lloyd</a>
*/
abstract class AbstractDelegatingSSLSocket extends SSLSocket {
private final SSLSocket delegate;
AbstractDelegatingSSLSocket(final SSLSocket delegate) {
this.delegate = delegate;
}
public String getApplicationProtocol() {
return delegate.getApplicationProtocol();
}
public String getHandshakeApplicationProtocol() {
return delegate.getHandshakeApplicationProtocol();
}
public void setHandshakeApplicationProtocolSelector(BiFunction<SSLSocket, List<String>, String> selector) {
delegate.setHandshakeApplicationProtocolSelector(selector);
}
public BiFunction<SSLSocket, List<String>, String> getHandshakeApplicationProtocolSelector() {
return delegate.getHandshakeApplicationProtocolSelector();
}
public String[] getSupportedCipherSuites() {
return delegate.getSupportedCipherSuites();
}
public String[] getEnabledCipherSuites() {
return delegate.getEnabledCipherSuites();
}
public void setEnabledCipherSuites(final String[] suites) {
delegate.setEnabledCipherSuites(suites);
}
public String[] getSupportedProtocols() {
return delegate.getSupportedProtocols();
}
public String[] getEnabledProtocols() {
return delegate.getEnabledProtocols();
}
public void setEnabledProtocols(final String[] protocols) {
delegate.setEnabledProtocols(protocols);
}
public SSLSession getSession() {
return delegate.getSession();
}
public SSLSession getHandshakeSession() {
return delegate.getHandshakeSession();
}
public void addHandshakeCompletedListener(final HandshakeCompletedListener listener) {
delegate.addHandshakeCompletedListener(listener);
}
public void removeHandshakeCompletedListener(final HandshakeCompletedListener listener) {
delegate.removeHandshakeCompletedListener(listener);
}
public void startHandshake() throws IOException {
delegate.startHandshake();
}
public void setUseClientMode(final boolean mode) {
delegate.setUseClientMode(mode);
}
public boolean getUseClientMode() {
return delegate.getUseClientMode();
}
public void setNeedClientAuth(final boolean need) {
delegate.setNeedClientAuth(need);
}
public boolean getNeedClientAuth() {
return delegate.getNeedClientAuth();
}
public void setWantClientAuth(final boolean want) {
delegate.setWantClientAuth(want);
}
public boolean getWantClientAuth() {
return delegate.getWantClientAuth();
}
public void setEnableSessionCreation(final boolean flag) {
delegate.setEnableSessionCreation(flag);
}
public boolean getEnableSessionCreation() {
return delegate.getEnableSessionCreation();
}
public SSLParameters getSSLParameters() {
return delegate.getSSLParameters();
}
public void setSSLParameters(final SSLParameters params) {
delegate.setSSLParameters(params);
}
public void connect(final SocketAddress endpoint) throws IOException {
delegate.connect(endpoint);
}
public void connect(final SocketAddress endpoint, final int timeout) throws IOException {
delegate.connect(endpoint, timeout);
}
public void bind(final SocketAddress bindpoint) throws IOException {
delegate.bind(bindpoint);
}
public InetAddress getInetAddress() {
return delegate.getInetAddress();
}
public InetAddress getLocalAddress() {
return delegate.getLocalAddress();
}
public int getPort() {
return delegate.getPort();
}
public int getLocalPort() {
return delegate.getLocalPort();
}
public SocketAddress getRemoteSocketAddress() {
return delegate.getRemoteSocketAddress();
}
public SocketAddress getLocalSocketAddress() {
return delegate.getLocalSocketAddress();
}
public SocketChannel getChannel() {
return delegate.getChannel();
}
public InputStream getInputStream() throws IOException {
return delegate.getInputStream();
}
public OutputStream getOutputStream() throws IOException {
return delegate.getOutputStream();
}
public void setTcpNoDelay(final boolean on) throws SocketException {
delegate.setTcpNoDelay(on);
}
public boolean getTcpNoDelay() throws SocketException {
return delegate.getTcpNoDelay();
}
public void setSoLinger(final boolean on, final int linger) throws SocketException {
delegate.setSoLinger(on, linger);
}
public int getSoLinger() throws SocketException {
return delegate.getSoLinger();
}
public void sendUrgentData(final int data) throws IOException {
delegate.sendUrgentData(data);
}
public void setOOBInline(final boolean on) throws SocketException {
delegate.setOOBInline(on);
}
public boolean getOOBInline() throws SocketException {
return delegate.getOOBInline();
}
public void setSoTimeout(final int timeout) throws SocketException {
delegate.setSoTimeout(timeout);
}
public int getSoTimeout() throws SocketException {
return delegate.getSoTimeout();
}
public void setSendBufferSize(final int size) throws SocketException {
delegate.setSendBufferSize(size);
}
public int getSendBufferSize() throws SocketException {
return delegate.getSendBufferSize();
}
public void setReceiveBufferSize(final int size) throws SocketException {
delegate.setReceiveBufferSize(size);
}
public int getReceiveBufferSize() throws SocketException {
return delegate.getReceiveBufferSize();
}
public void setKeepAlive(final boolean on) throws SocketException {
delegate.setKeepAlive(on);
}
public boolean getKeepAlive() throws SocketException {
return delegate.getKeepAlive();
}
public void setTrafficClass(final int tc) throws SocketException {
delegate.setTrafficClass(tc);
}
public int getTrafficClass() throws SocketException {
return delegate.getTrafficClass();
}
public void setReuseAddress(final boolean on) throws SocketException {
delegate.setReuseAddress(on);
}
public boolean getReuseAddress() throws SocketException {
return delegate.getReuseAddress();
}
public void close() throws IOException {
delegate.close();
}
public void shutdownInput() throws IOException {
delegate.shutdownInput();
}
public void shutdownOutput() throws IOException {
delegate.shutdownOutput();
}
public String toString() {
return delegate.toString();
}
public boolean isConnected() {
return delegate.isConnected();
}
public boolean isBound() {
return delegate.isBound();
}
public boolean isClosed() {
return delegate.isClosed();
}
public boolean isInputShutdown() {
return delegate.isInputShutdown();
}
public boolean isOutputShutdown() {
return delegate.isOutputShutdown();
}
public void setPerformancePreferences(final int connectionTime, final int latency, final int bandwidth) {
delegate.setPerformancePreferences(connectionTime, latency, bandwidth);
}
protected SSLSocket getDelegate() {
return delegate;
}
}

View file

@ -1,6 +1,11 @@
package org.keycloak.crypto.fips; package org.keycloak.crypto.fips;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket; import java.net.Socket;
import java.net.SocketAddress;
import java.net.UnknownHostException;
import java.security.InvalidAlgorithmParameterException; import java.security.InvalidAlgorithmParameterException;
import java.security.KeyFactory; import java.security.KeyFactory;
import java.security.KeyPairGenerator; import java.security.KeyPairGenerator;
@ -57,10 +62,7 @@ import org.keycloak.common.crypto.PemUtilsProvider;
import org.keycloak.common.crypto.UserIdentityExtractorProvider; import org.keycloak.common.crypto.UserIdentityExtractorProvider;
import org.keycloak.common.util.BouncyIntegration; import org.keycloak.common.util.BouncyIntegration;
import org.keycloak.common.util.KeystoreUtil.KeystoreFormat; import org.keycloak.common.util.KeystoreUtil.KeystoreFormat;
import org.keycloak.common.util.Resteasy;
import org.keycloak.crypto.JavaAlgorithm; import org.keycloak.crypto.JavaAlgorithm;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
/** /**
@ -225,26 +227,73 @@ public class FIPS1402Provider implements CryptoProvider {
@Override @Override
public SSLSocketFactory wrapFactoryForTruststore(SSLSocketFactory delegate) { public SSLSocketFactory wrapFactoryForTruststore(SSLSocketFactory delegate) {
KeycloakSession session = Resteasy.getProvider().getContextData(KeycloakSession.class);
if (session == null) {
log.tracef("Not found keycloakSession in the resteasy context when trying to retrieve hostname attribute from it");
return delegate;
}
String hostname = session.getAttribute(Constants.SSL_SERVER_HOST_ATTR, String.class);
log.tracef("Found hostname '%s' to be used by SSLSocketFactory", hostname);
if (hostname == null) return delegate;
// See https://downloads.bouncycastle.org/fips-java/BC-FJA-(D)TLSUserGuide-1.0.9.pdf - Section 3.5.2 (Endpoint identification) // See https://downloads.bouncycastle.org/fips-java/BC-FJA-(D)TLSUserGuide-1.0.9.pdf - Section 3.5.2 (Endpoint identification)
return new CustomSSLSocketFactory(delegate) { return new CustomSSLSocketFactory(delegate) {
@Override
public Socket createSocket() throws IOException {
// Creating unconnected socket (Used for example by com.sun.jndi.ldap.Connection.createSocket - when connectionTimeout > 0)
// Configuration of SNI hostname needs to be postponed as we don't yet know the hostname
Socket socket = delegate.createSocket();
if (socket instanceof SSLSocket) {
return new AbstractDelegatingSSLSocket((SSLSocket) socket) {
@Override
public void connect(SocketAddress endpoint) throws IOException {
log.tracef("Calling connect(%s)", endpoint);
if (endpoint instanceof InetSocketAddress) {
configureSocket(getDelegate(), ((InetSocketAddress) endpoint).getHostName());
}
super.connect(endpoint);
}
@Override
public void connect(SocketAddress endpoint, int timeout) throws IOException {
log.tracef("Calling connect(%s, %d)", endpoint, timeout);
if (endpoint instanceof InetSocketAddress) {
configureSocket(getDelegate(), ((InetSocketAddress) endpoint).getHostName());
}
super.connect(endpoint, timeout);
}
};
}
return socket;
}
@Override
public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
return configureSocket(delegate.createSocket(host, port), host);
}
@Override
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {
return configureSocket(delegate.createSocket(host, port, localHost, localPort), host);
}
@Override @Override
protected Socket configureSocket(Socket s) { protected Socket configureSocket(Socket s) {
if (s instanceof SSLSocket) {
if (s.getInetAddress() == null) {
throw new IllegalArgumentException("Socket not connected before trying to configure SSL Hostname");
}
String hostname = s.getInetAddress().getHostName();
configureSocket(s, hostname);
}
return s;
}
private Socket configureSocket(Socket s, String hostname) {
if (s instanceof SSLSocket) { if (s instanceof SSLSocket) {
SSLSocket ssl = (SSLSocket)s; SSLSocket ssl = (SSLSocket)s;
SNIHostName sniHostName = getSNIHostName(hostname); SNIHostName sniHostname = getSNIHostName(hostname);
if (sniHostName != null) { log.tracef("Configuration of SSL Socket - using sniHostname '%s' for the socket host '%s'", sniHostname, hostname);
SSLParameters sslParameters = new SSLParameters();
sslParameters.setServerNames(Collections.singletonList(sniHostName)); if (sniHostname != null) {
SSLParameters sslParameters = ssl.getSSLParameters();
if (sslParameters == null) {
sslParameters = new SSLParameters();
}
sslParameters.setServerNames(Collections.singletonList(sniHostname));
ssl.setSSLParameters(sslParameters); ssl.setSSLParameters(sslParameters);
} }
} }

View file

@ -32,11 +32,8 @@ import java.util.stream.Collectors;
import javax.naming.directory.SearchControls; import javax.naming.directory.SearchControls;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.common.util.UriUtils;
import org.keycloak.component.ComponentModel; import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException; import org.keycloak.component.ComponentValidationException;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.LDAPConstants; import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelException; import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
@ -380,10 +377,4 @@ public class LDAPUtils {
return userModelProperties; return userModelProperties;
} }
public static void setLDAPHostnameToKeycloakSession(KeycloakSession session,LDAPConfig ldapConfig) {
String hostname = UriUtils.getHost(ldapConfig.getConnectionUrl());
session.setAttribute(Constants.SSL_SERVER_HOST_ATTR, hostname);
log.tracef("Setting LDAP server hostname '%s' as KeycloakSession attribute", hostname);
}
} }

View file

@ -4,7 +4,6 @@ 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.storage.ldap.LDAPUtils;
import org.keycloak.truststore.TruststoreProvider; import org.keycloak.truststore.TruststoreProvider;
import org.keycloak.vault.VaultCharSecret; import org.keycloak.vault.VaultCharSecret;
@ -67,8 +66,6 @@ public final class LDAPContextManager implements AutoCloseable {
} }
private void createLdapContext() throws NamingException { private void createLdapContext() throws NamingException {
LDAPUtils.setLDAPHostnameToKeycloakSession(session, ldapConfig);
Hashtable<Object, Object> connProp = getConnectionProperties(ldapConfig); Hashtable<Object, Object> connProp = getConnectionProperties(ldapConfig);
if (!LDAPConstants.AUTH_TYPE_NONE.equals(ldapConfig.getAuthType())) { if (!LDAPConstants.AUTH_TYPE_NONE.equals(ldapConfig.getAuthType())) {

View file

@ -23,7 +23,6 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.LDAPConstants; import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelException; import org.keycloak.models.ModelException;
import org.keycloak.storage.ldap.LDAPConfig; import org.keycloak.storage.ldap.LDAPConfig;
import org.keycloak.storage.ldap.LDAPUtils;
import org.keycloak.storage.ldap.idm.model.LDAPDn; 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;
@ -497,8 +496,6 @@ public class LDAPOperationManager {
StartTlsResponse tlsResponse = null; StartTlsResponse tlsResponse = null;
try { try {
LDAPUtils.setLDAPHostnameToKeycloakSession(session, config);
Hashtable<Object, Object> env = LDAPContextManager.getNonAuthConnectionProperties(config); Hashtable<Object, Object> env = LDAPContextManager.getNonAuthConnectionProperties(config);
// Never use connection pool to prevent password caching // Never use connection pool to prevent password caching

View file

@ -82,8 +82,6 @@ public final class LdapMapContextManager implements AutoCloseable {
} }
private void createLdapContext() throws NamingException { private void createLdapContext() throws NamingException {
LdapMapUtil.setLDAPHostnameToKeycloakSession(session, ldapMapConfig);
Hashtable<Object, Object> connProp = getConnectionProperties(ldapMapConfig); Hashtable<Object, Object> connProp = getConnectionProperties(ldapMapConfig);
if (!LDAPConstants.AUTH_TYPE_NONE.equals(ldapMapConfig.getAuthType())) { if (!LDAPConstants.AUTH_TYPE_NONE.equals(ldapMapConfig.getAuthType())) {

View file

@ -390,8 +390,6 @@ public class LdapMapOperationManager implements AutoCloseable {
StartTlsResponse tlsResponse = null; StartTlsResponse tlsResponse = null;
try { try {
LdapMapUtil.setLDAPHostnameToKeycloakSession(session, config);
Hashtable<Object, Object> env = LdapMapContextManager.getNonAuthConnectionProperties(config); Hashtable<Object, Object> env = LdapMapContextManager.getNonAuthConnectionProperties(config);
// Never use connection pool to prevent password caching // Never use connection pool to prevent password caching

View file

@ -18,11 +18,7 @@
package org.keycloak.models.map.storage.ldap.store; package org.keycloak.models.map.storage.ldap.store;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.common.util.UriUtils;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelException; import org.keycloak.models.ModelException;
import org.keycloak.models.map.storage.ldap.config.LdapMapConfig;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
@ -257,11 +253,4 @@ public class LdapMapUtil {
} }
} }
public static void setLDAPHostnameToKeycloakSession(KeycloakSession session, LdapMapConfig ldapConfig) {
String hostname = UriUtils.getHost(ldapConfig.getConnectionUrl());
session.setAttribute(Constants.SSL_SERVER_HOST_ATTR, hostname);
logger.tracef("Setting LDAP server hostname '%s' as KeycloakSession attribute", hostname);
}
} }

View file

@ -147,8 +147,4 @@ public final class Constants {
public static final Boolean REALM_ATTR_USERNAME_CASE_SENSITIVE_DEFAULT = Boolean.FALSE; public static final Boolean REALM_ATTR_USERNAME_CASE_SENSITIVE_DEFAULT = Boolean.FALSE;
public static final String REALM_ATTR_USERNAME_CASE_SENSITIVE = "keycloak.username-search.case-sensitive"; public static final String REALM_ATTR_USERNAME_CASE_SENSITIVE = "keycloak.username-search.case-sensitive";
/**
* Attribute of KeycloakSession where the hostname of the SSL server can be saved. This can be used by org.keycloak.truststore.SSLSocketFactory
*/
public static final String SSL_SERVER_HOST_ATTR = "sslServerHost";
} }

View file

@ -17,6 +17,9 @@
package org.keycloak.models; package org.keycloak.models;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
@ -163,11 +166,24 @@ public class LDAPConstants {
} else if (useTruststoreSpi != null && useTruststoreSpi.equals(LDAPConstants.USE_TRUSTSTORE_NEVER)) { } else if (useTruststoreSpi != null && useTruststoreSpi.equals(LDAPConstants.USE_TRUSTSTORE_NEVER)) {
shouldSetTruststore = false; shouldSetTruststore = false;
} else { } else {
shouldSetTruststore = (url != null && url.toLowerCase().startsWith("ldaps")); shouldSetTruststore = toLdapUrls(url).stream()
.anyMatch(urlPart -> urlPart.toLowerCase().startsWith("ldaps"));
} }
if (shouldSetTruststore) { if (shouldSetTruststore) {
env.put("java.naming.ldap.factory.socket", "org.keycloak.truststore.SSLSocketFactory"); env.put("java.naming.ldap.factory.socket", "org.keycloak.truststore.SSLSocketFactory");
} }
} }
/**
* @see com.sun.jndi.ldap.LdapURL#fromList(String) (Not using it directly to avoid usage of internal Java classes)
*
* @param ldapUrlList LDAP URL, which can possibly consists from multiple URLs like "ldaps://host1:636 ldaps://host2:636"
* @return List of all URLs
*/
public static List<String> toLdapUrls(String ldapUrlList) {
if (ldapUrlList == null) return Collections.emptyList();
return Arrays.asList(ldapUrlList.split(" "));
}
} }

View file

@ -108,6 +108,34 @@ public class UserFederationLdapConnectionTest extends AbstractAdminTest {
assertStatus(response, 204); assertStatus(response, 204);
} }
@Test
public void testLdapConnectionMoreServers() {
// Both servers work
Response response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPServerCapabilitiesManager.TEST_AUTHENTICATION, "ldap://localhost:10389 ldaps://localhost:10636", "uid=admin,ou=system", "secret", "true", null));
assertStatus(response, 204);
// Only 1st server works
response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPServerCapabilitiesManager.TEST_AUTHENTICATION, "ldap://localhost:10389 ldap://localhostt:10389", "uid=admin,ou=system", "secret", "true", null));
assertStatus(response, 204);
// Only 1st server works - variant with connectionTimeout (important to test as com.sun.jndi.ldap.Connection.createSocket implementation differs based on whether connectionTimeout is used)
response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPServerCapabilitiesManager.TEST_AUTHENTICATION, "ldap://localhost:10389 ldap://localhostt:10389", "uid=admin,ou=system", "secret", "true", "10000"));
assertStatus(response, 204);
// Only 2nd server works
response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPServerCapabilitiesManager.TEST_AUTHENTICATION, "ldap://localhostt:10389 ldaps://localhost:10636", "uid=admin,ou=system", "secret", "true", null));
assertStatus(response, 204);
// Only 2nd server works - variant with connectionTimeout
response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPServerCapabilitiesManager.TEST_AUTHENTICATION, "ldap://localhostt:10389 ldaps://localhost:10636", "uid=admin,ou=system", "secret", "true", "10000"));
assertStatus(response, 204);
// None of servers work
response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPServerCapabilitiesManager.TEST_AUTHENTICATION, "ldap://localhostt:10389 ldaps://localhostt:10636", "uid=admin,ou=system", "secret", "true", null));
assertStatus(response, 400);
}
@Test @Test
public void testLdapCapabilities() { public void testLdapCapabilities() {