Skip to content

Commit 4241521

Browse files
committed
LDAP SASL EXTERNAL with KeyStore SPI
Signed-off-by: Tero Saarni <[email protected]>
1 parent 2417350 commit 4241521

File tree

11 files changed

+608
-118
lines changed

11 files changed

+608
-118
lines changed

federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPContextManager.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ public void close() {
5959
public LDAPContextManager(KeycloakSession session, LDAPConfig connectionProperties) {
6060
this.session = session;
6161
this.ldapConfig = connectionProperties;
62+
63+
String useTruststoreSpi = connectionProperties.getUseTruststoreSpi();
64+
if (useTruststoreSpi != null && !useTruststoreSpi.equals(LDAPConstants.USE_TRUSTSTORE_NEVER)) {
65+
// Initialize LDAP socket factory that utilizes TrustStore SPI and KeyStore SPI.
66+
LDAPSSLSocketFactory.initialize(session);
67+
}
6268
}
6369

6470
public static LDAPContextManager create(KeycloakSession session, LDAPConfig connectionProperties) {
@@ -81,8 +87,7 @@ private void createLdapContext() throws NamingException {
8187
if (ldapConfig.isStartTls()) {
8288
SSLSocketFactory sslSocketFactory = null;
8389
if (LDAPUtil.shouldUseTruststoreSpi(ldapConfig)) {
84-
TruststoreProvider provider = session.getProvider(TruststoreProvider.class);
85-
sslSocketFactory = provider.getSSLSocketFactory();
90+
sslSocketFactory = LDAPSSLSocketFactory.getDefault();
8691
}
8792

8893
tlsResponse = startTLS(ldapContext, ldapConfig.getAuthType(), ldapConfig.getBindDN(),
@@ -193,7 +198,7 @@ public static Hashtable<Object, Object> getNonAuthConnectionProperties(LDAPConfi
193198
// when using Start TLS, use default socket factory for LDAP client but pass the TrustStore SSL socket factory later
194199
// when calling StartTlsResponse.negotiate(trustStoreSSLSocketFactory)
195200
if (!ldapConfig.isStartTls() && LDAPUtil.shouldUseTruststoreSpi(ldapConfig)) {
196-
env.put("java.naming.ldap.factory.socket", "org.keycloak.truststore.SSLSocketFactory");
201+
env.put("java.naming.ldap.factory.socket", "org.keycloak.storage.ldap.idm.store.ldap.LDAPSSLSocketFactory");
197202
}
198203

199204
String connectionPooling = ldapConfig.getConnectionPooling();

federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
2828
import org.keycloak.storage.ldap.idm.store.ldap.extended.PasswordModifyRequest;
2929
import org.keycloak.storage.ldap.mappers.LDAPOperationDecorator;
30-
import org.keycloak.truststore.TruststoreProvider;
3130

3231
import javax.naming.AuthenticationException;
3332
import javax.naming.Binding;
@@ -511,8 +510,10 @@ public void authenticate(LdapName dn, String password) throws AuthenticationExce
511510
if (config.isStartTls()) {
512511
SSLSocketFactory sslSocketFactory = null;
513512
if (LDAPUtil.shouldUseTruststoreSpi(config)) {
514-
TruststoreProvider provider = session.getProvider(TruststoreProvider.class);
515-
sslSocketFactory = provider.getSSLSocketFactory();
513+
// In this code path LDAPContextManager is not used, so we'd need to make sure that
514+
// the SSL socket factory has been initialized before using.
515+
LDAPSSLSocketFactory.initialize(session);
516+
sslSocketFactory = LDAPSSLSocketFactory.getDefault();
516517
}
517518

518519
tlsResponse = LDAPContextManager.startTLS(authCtx, "simple", dn.toString(), password.toCharArray(), sslSocketFactory);
@@ -532,7 +533,7 @@ public void authenticate(LdapName dn, String password) throws AuthenticationExce
532533
if (logger.isDebugEnabled()) {
533534
logger.debugf(re, "LDAP Connection TimeOut for DN [%s]", dn);
534535
}
535-
536+
536537
throw re;
537538

538539
} catch (Exception e) {
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*
2+
* Copyright 2022 Red Hat, Inc. and/or its affiliates
3+
* and other contributors as indicated by the @author tags.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.keycloak.storage.ldap.idm.store.ldap;
19+
20+
import org.jboss.logging.Logger;
21+
import org.keycloak.keystore.KeyStoreProvider;
22+
import org.keycloak.models.KeycloakSession;
23+
import org.keycloak.truststore.TruststoreProvider;
24+
25+
import java.io.IOException;
26+
import java.net.InetAddress;
27+
import java.net.Socket;
28+
import java.security.InvalidAlgorithmParameterException;
29+
import java.security.KeyManagementException;
30+
import java.security.KeyStore;
31+
import java.security.KeyStoreException;
32+
import java.security.NoSuchAlgorithmException;
33+
import java.util.Comparator;
34+
35+
import javax.net.ssl.KeyManager;
36+
import javax.net.ssl.KeyManagerFactory;
37+
import javax.net.ssl.KeyStoreBuilderParameters;
38+
import javax.net.ssl.SSLContext;
39+
import javax.net.ssl.SSLSocketFactory;
40+
import javax.net.ssl.TrustManager;
41+
import javax.net.ssl.TrustManagerFactory;
42+
43+
44+
public class LDAPSSLSocketFactory extends SSLSocketFactory implements Comparator<String> {
45+
46+
private static final String NOT_IMPLEMENTED_BY_LDAP_SOCKET_FACTORY = "Not implemented by LDAPSSLSocketFactory";
47+
48+
private static final Logger log = Logger.getLogger(LDAPSSLSocketFactory.class);
49+
50+
private static SSLSocketFactory instance = null;
51+
52+
private LDAPSSLSocketFactory() {
53+
}
54+
55+
public static synchronized void initialize(KeycloakSession session) {
56+
if (instance == null) {
57+
try {
58+
// Initialize TrustManager with TrustStore provided by TrustStore SPI.
59+
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
60+
TruststoreProvider tsp = session.getProvider(TruststoreProvider.class);
61+
if (tsp == null) {
62+
new RuntimeException("Truststore SPI used but Truststore was not configured");
63+
}
64+
tmf.init(tsp.getTruststore());
65+
TrustManager[] tms = tmf.getTrustManagers();
66+
67+
// Initialize KeyManager with KeyStore provided by KeyStore SPI.
68+
KeyManager[] kms = null;
69+
KeyManagerFactory kmf = KeyManagerFactory.getInstance("NewSunX509");
70+
KeyStore.Builder ksb = session.getProvider(KeyStoreProvider.class)
71+
.loadKeyStoreBuilder(KeyStoreProvider.LDAP_CLIENT_KEYSTORE);
72+
if (ksb != null) {
73+
kmf.init(new KeyStoreBuilderParameters(ksb));
74+
kms = kmf.getKeyManagers();
75+
}
76+
77+
log.infov("Initializing LDAPSSLSocketFactory: trustStore={0}, keyStore={1}",
78+
tms != null ? "yes" : "no",
79+
kms != null ? "yes" : "no");
80+
81+
SSLContext context = SSLContext.getInstance("TLS");
82+
context.init(kms, tms, null);
83+
84+
instance = context.getSocketFactory();
85+
} catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException | InvalidAlgorithmParameterException e) {
86+
log.error("Failed to initialize SSLContext for LDAP: " + e.toString());
87+
throw new RuntimeException("Failed to initialize SSLContext: " + e.toString());
88+
}
89+
}
90+
}
91+
92+
public static SSLSocketFactory getDefault() {
93+
return instance;
94+
}
95+
96+
/**
97+
* Enables LDAP connection pooling for sockets from custom socket factory.
98+
* See https://docs.oracle.com/javase/8/docs/technotes/guides/jndi/jndi-ldap.html#pooling
99+
*
100+
* Note that Comparator<String> needs to be implemented since JDK uses the class name as string for the comparison.
101+
*
102+
* For more information, see:
103+
* https://stackoverflow.com/questions/23898970/pooling-ldap-connections-with-custom-socket-factory
104+
* https://bugs.openjdk.org/browse/JDK-6587244?page=com.atlassian.jira.plugin.system.issuetabpanels%3Aworklog-tabpanel
105+
*/
106+
@Override
107+
public int compare(String o1, String o2) {
108+
return o1.compareTo(o2);
109+
}
110+
111+
// Following methods are not used by the JNDI LDAP implementation and therefore do not need to be implemented.
112+
113+
@Override
114+
public Socket createSocket(String host, int port) throws IOException {
115+
throw new UnsupportedOperationException(NOT_IMPLEMENTED_BY_LDAP_SOCKET_FACTORY);
116+
}
117+
118+
@Override
119+
public Socket createSocket(String host, int port, InetAddress localHost, int localPort)
120+
throws IOException {
121+
throw new UnsupportedOperationException(NOT_IMPLEMENTED_BY_LDAP_SOCKET_FACTORY);
122+
}
123+
124+
@Override
125+
public Socket createSocket(InetAddress host, int port) throws IOException {
126+
throw new UnsupportedOperationException(NOT_IMPLEMENTED_BY_LDAP_SOCKET_FACTORY);
127+
}
128+
129+
@Override
130+
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort)
131+
throws IOException {
132+
throw new UnsupportedOperationException(NOT_IMPLEMENTED_BY_LDAP_SOCKET_FACTORY);
133+
}
134+
135+
@Override
136+
public String[] getDefaultCipherSuites() {
137+
throw new UnsupportedOperationException(NOT_IMPLEMENTED_BY_LDAP_SOCKET_FACTORY);
138+
}
139+
140+
@Override
141+
public String[] getSupportedCipherSuites() {
142+
throw new UnsupportedOperationException(NOT_IMPLEMENTED_BY_LDAP_SOCKET_FACTORY);
143+
}
144+
145+
@Override
146+
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
147+
throw new UnsupportedOperationException(NOT_IMPLEMENTED_BY_LDAP_SOCKET_FACTORY);
148+
}
149+
150+
}

server-spi-private/src/main/java/org/keycloak/models/LDAPConstants.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public class LDAPConstants {
5555
public static final String AUTH_TYPE = "authType";
5656
public static final String AUTH_TYPE_NONE = "none";
5757
public static final String AUTH_TYPE_SIMPLE = "simple";
58+
public static final String AUTH_TYPE_EXTERNAL = "EXTERNAL";
5859

5960
public static final String USE_TRUSTSTORE_SPI = "useTruststoreSpi";
6061
public static final String USE_TRUSTSTORE_ALWAYS = "always";

testsuite/integration-arquillian/servers/auth-server/quarkus/src/main/content/conf/keycloak.conf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ spi-hostname-default-frontend-url = ${keycloak.frontendUrl:}
2828
spi-truststore-file-file=${kc.home.dir}/conf/keycloak.truststore
2929
spi-truststore-file-password=secret
3030

31+
# KeyStore Provider
32+
spi-keystore-default-ldap-keystore-file=${kc.home.dir}/conf/keycloak.jks
33+
spi-keystore-default-ldap-keystore-password=secret
34+
3135
# Declarative User Profile
3236
spi-user-profile-provider=declarative-user-profile
3337
spi-user-profile-declarative-user-profile-read-only-attributes=deniedFoo,deniedBar*,deniedSome/thing,deniedsome*thing

0 commit comments

Comments
 (0)