Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions federation/ldap/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@
<artifactId>jakarta.transaction-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-tls-registry</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<!-- needed for InMemoryUserAdapter -->
<groupId>org.keycloak</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ private static int parseConnectionTimeout(String connectionTimeout) {
public static LDAPConfig buildLDAPConfig(TestLdapConnectionRepresentation config, RealmModel realm) {
String bindCredential = config.getBindCredential();
if (config.getComponentId() != null && !LDAPConstants.AUTH_TYPE_NONE.equals(config.getAuthType())
&& !LDAPConstants.AUTH_TYPE_EXTERNAL.equals(config.getAuthType())
&& ComponentRepresentation.SECRET_VALUE.equals(bindCredential)) {
// check the connection URL and the bind DN are the same to allow using the same configured password
ComponentModel component = realm.getComponent(config.getComponentId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ private void createLdapContext() throws NamingException {
// Send StartTLS request and setup SSL context if needed.
if (ldapConfig.isStartTls()) {
SSLSocketFactory sslSocketFactory = null;
if (LDAPUtil.shouldUseTruststoreSpi(ldapConfig)) {
if (LDAPConstants.AUTH_TYPE_EXTERNAL.equals(ldapConfig.getAuthType())) {
sslSocketFactory = LDAPSSLSocketFactory.getDefault();
} else if (LDAPUtil.shouldUseTruststoreSpi(ldapConfig)) {
TruststoreProvider provider = session.getProvider(TruststoreProvider.class);
sslSocketFactory = provider.getSSLSocketFactory();
}
Expand Down Expand Up @@ -123,14 +125,16 @@ private void setAdminConnectionAuthProperties(LdapContext ldapContext) throws Na
ldapContext.addToEnvironment(Context.SECURITY_AUTHENTICATION, authType);
}

String bindPassword = getBindPassword();
if (bindPassword != null) {
ldapContext.addToEnvironment(SECURITY_CREDENTIALS, bindPassword);
}
if (!LDAPConstants.AUTH_TYPE_EXTERNAL.equals(authType)) {
String bindPassword = getBindPassword();
if (bindPassword != null) {
ldapContext.addToEnvironment(SECURITY_CREDENTIALS, bindPassword);
}

String bindDN = ldapConfig.getBindDN();
if (bindDN != null) {
ldapContext.addToEnvironment(Context.SECURITY_PRINCIPAL, bindDN);
String bindDN = ldapConfig.getBindDN();
if (bindDN != null) {
ldapContext.addToEnvironment(Context.SECURITY_PRINCIPAL, bindDN);
}
}

if (logger.isDebugEnabled()) {
Expand All @@ -142,7 +146,6 @@ private void setAdminConnectionAuthProperties(LdapContext ldapContext) throws Na
}
}


/**
* This method is used for admin connection and user authentication. Hence it returns just connection properties NOT related to
* authentication (properties like bindType, bindDn, bindPassword). Caller of this method needs to fill auth-related connection properties
Expand All @@ -166,8 +169,13 @@ public static Hashtable<Object, Object> getNonAuthConnectionProperties(LDAPConfi

// 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() && LDAPUtil.shouldUseTruststoreSpi(ldapConfig)) {
env.put("java.naming.ldap.factory.socket", "org.keycloak.truststore.SSLSocketFactory");
if (!ldapConfig.isStartTls()) {
if (LDAPConstants.AUTH_TYPE_EXTERNAL.equals(ldapConfig.getAuthType())) {
env.put("java.naming.ldap.factory.socket",
"org.keycloak.storage.ldap.idm.store.ldap.LDAPSSLSocketFactory");
} else if (LDAPUtil.shouldUseTruststoreSpi(ldapConfig)) {
env.put("java.naming.ldap.factory.socket", "org.keycloak.truststore.SSLSocketFactory");
}
}

String connectionPooling = ldapConfig.getConnectionPooling();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* Copyright 2026 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.storage.ldap.idm.store.ldap;

import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.util.Comparator;
import java.util.function.Supplier;
import javax.net.ssl.SSLSocketFactory;

/**
* SSLSocketFactory for LDAP connections that obtains a fresh SSLSocketFactory
* from a configured supplier on each createSocket() call.
*/
public class LDAPSSLSocketFactory extends SSLSocketFactory implements Comparator<String> {

private static volatile Supplier<SSLSocketFactory> sslSocketFactorySupplier;

public static void setSSLSocketFactorySupplier(Supplier<SSLSocketFactory> supplier) {
sslSocketFactorySupplier = supplier;
}

public static SSLSocketFactory getDefault() {
return new LDAPSSLSocketFactory();
}

private SSLSocketFactory delegate() {
Supplier<SSLSocketFactory> supplier = sslSocketFactorySupplier;
if (supplier == null) {
throw new IllegalStateException("LDAPSSLSocketFactory has no SSLSocketFactory supplier configured");
}
return supplier.get();
}

@Override
public Socket createSocket() throws IOException {
return delegate().createSocket();
}

@Override
public Socket createSocket(String host, int port) throws IOException {
return delegate().createSocket(host, port);
}

@Override
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
return delegate().createSocket(host, port, localHost, localPort);
}

@Override
public Socket createSocket(InetAddress host, int port) throws IOException {
return delegate().createSocket(host, port);
}

@Override
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
return delegate().createSocket(address, port, localAddress, localPort);
}

@Override
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
return delegate().createSocket(s, host, port, autoClose);
}

@Override
public String[] getDefaultCipherSuites() {
return delegate().getDefaultCipherSuites();
}

@Override
public String[] getSupportedCipherSuites() {
return delegate().getSupportedCipherSuites();
}

@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ updateCredentialUserLabelSuccess=The user label has been changed successfully.
product=Product
credentialUserLabel=User Label
passwordPoliciesHelp.passwordBlacklist=Prevents users to log in with a forbidden password that is listed in a file on the server.
bindTypeHelp=Type of the authentication method used during LDAP bind operation. It is used in most of the requests sent to the LDAP server. Currently only 'none' (anonymous LDAP authentication) or 'simple' (bind credential + bind password authentication) mechanisms are available.
bindTypeHelp=Type of the authentication method used during LDAP bind operation. It is used in most of the requests sent to the LDAP server. Available mechanisms: 'none' (anonymous LDAP authentication), 'simple' (bind credential + bind password authentication), or 'EXTERNAL' (SASL EXTERNAL authentication using a client certificate).
whoWillAppearPopoverText=Groups are hierarchical. When you select Direct Membership, you see only the child group that the user joined. Ancestor groups are not included.
eventTypes.VERIFY_EMAIL.description=Verify email
eventTypes.REFRESH_TOKEN_ERROR.name=Refresh token error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ export const LdapSettingsConnection = ({
>
<SelectOption value="simple">simple</SelectOption>
<SelectOption value="none">none</SelectOption>
<SelectOption value="EXTERNAL">EXTERNAL</SelectOption>
</KeycloakSelect>
)}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright 2026 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.quarkus.runtime.integration;

import java.util.Optional;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;

import org.keycloak.storage.ldap.idm.store.ldap.LDAPSSLSocketFactory;

import io.quarkus.runtime.StartupEvent;
import io.quarkus.tls.TlsConfiguration;
import io.quarkus.tls.TlsConfigurationRegistry;
import org.jboss.logging.Logger;

/**
* Initializes LDAP socket factory with client certificate TLS configuration from the Quarkus TLS Registry.
* If a TLS configuration named "ldap" is present, client certificates are used for SASL EXTERNAL authentication with LDAP.
*/
@ApplicationScoped
public class LdapTlsRegistryInitializer {

private static final Logger logger = Logger.getLogger(LdapTlsRegistryInitializer.class);
private static final String TLS_CONFIG_NAME = "ldap";

void onStart(@Observes StartupEvent event, TlsConfigurationRegistry registry) {
Optional<TlsConfiguration> tlsConfig = registry.get(TLS_CONFIG_NAME);
if (tlsConfig.isPresent()) {
TlsConfiguration config = tlsConfig.get();
LDAPSSLSocketFactory.setSSLSocketFactorySupplier(() -> {
try {
return config.createSSLContext().getSocketFactory();
} catch (Exception e) {
throw new RuntimeException("Failed to create SSLContext from TLS Registry '" + TLS_CONFIG_NAME + "' configuration", e);
}
});
logger.debug("Loaded LDAP client certificate configuration from Quarkus TLS Registry and configured socket factory for SASL EXTERNAL authentication");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public class LDAPConstants {
public static final String AUTH_TYPE = "authType";
public static final String AUTH_TYPE_NONE = "none";
public static final String AUTH_TYPE_SIMPLE = "simple";
public static final String AUTH_TYPE_EXTERNAL = "EXTERNAL";

public static final String USE_TRUSTSTORE_SPI = "useTruststoreSpi";
public static final String USE_TRUSTSTORE_ALWAYS = "always";
Expand Down
Loading