Fix the issue with LDAP connectionUrl containing multiple hosts
Closes #17359
This commit is contained in:
parent
d7c3678096
commit
1cbdf4d17e
12 changed files with 434 additions and 51 deletions
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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())) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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";
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(" "));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue