Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,28 @@

package org.springframework.boot.ldap.autoconfigure.embedded;

import java.io.IOException;
import java.io.InputStream;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;

import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
Expand All @@ -33,6 +49,7 @@
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
Expand All @@ -47,6 +64,8 @@
import org.springframework.boot.ldap.autoconfigure.LdapAutoConfiguration;
import org.springframework.boot.ldap.autoconfigure.LdapProperties;
import org.springframework.boot.ldap.autoconfigure.embedded.EmbeddedLdapAutoConfiguration.EmbeddedLdapAutoConfigurationRuntimeHints;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
Expand All @@ -60,9 +79,12 @@
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.ldap.core.ContextSource;
import org.springframework.ldap.core.support.LdapContextSource;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
Expand All @@ -84,14 +106,18 @@ public final class EmbeddedLdapAutoConfiguration implements DisposableBean {

private final EmbeddedLdapProperties embeddedProperties;

private final ResourceLoader resourceLoader = new PathMatchingResourcePatternResolver();

private @Nullable InMemoryDirectoryServer server;

EmbeddedLdapAutoConfiguration(EmbeddedLdapProperties embeddedProperties) {
this.embeddedProperties = embeddedProperties;
}

@Bean
InMemoryDirectoryServer directoryServer(ApplicationContext applicationContext) throws LDAPException {
InMemoryDirectoryServer directoryServer(ApplicationContext applicationContext,
ObjectProvider<SslBundles> sslBundles) throws LDAPException, KeyStoreException, IOException,
NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException, KeyManagementException {
String[] baseDn = StringUtils.toStringArray(this.embeddedProperties.getBaseDn());
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(baseDn);
String username = this.embeddedProperties.getCredential().getUsername();
Expand All @@ -100,9 +126,18 @@ InMemoryDirectoryServer directoryServer(ApplicationContext applicationContext) t
config.addAdditionalBindCredentials(username, password);
}
setSchema(config);
InMemoryListenerConfig listenerConfig = InMemoryListenerConfig.createLDAPConfig("LDAP",
this.embeddedProperties.getPort());
config.setListenerConfigs(listenerConfig);
if (this.embeddedProperties.getSsl().isEnabled()) {
EmbeddedLdapProperties.Ssl ssl = this.embeddedProperties.getSsl();
SSLContext sslContext = getSslContext(ssl, sslBundles.getIfAvailable());
SSLServerSocketFactory serverSocketFactory = sslContext.getServerSocketFactory();
SSLSocketFactory clientSocketFactory = sslContext.getSocketFactory();
config.setListenerConfigs(InMemoryListenerConfig.createLDAPSConfig("LDAPS", null,
this.embeddedProperties.getPort(), serverSocketFactory, clientSocketFactory));
}
else {
config
.setListenerConfigs(InMemoryListenerConfig.createLDAPConfig("LDAP", this.embeddedProperties.getPort()));
}
this.server = new InMemoryDirectoryServer(config);
importLdif(this.server, applicationContext);
this.server.startListening();
Expand Down Expand Up @@ -181,6 +216,70 @@ public void destroy() throws Exception {
}
}

private SSLContext getSslContext(EmbeddedLdapProperties.Ssl ssl, @Nullable SslBundles sslBundles)
throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException,
UnrecoverableKeyException, KeyManagementException {
if (sslBundles != null && StringUtils.hasText(ssl.getBundle())) {
SslBundle sslBundle = sslBundles.getBundle(ssl.getBundle());
Assert.notNull(sslBundle, "SSL bundle name has been set but no SSL bundles found in context");
return sslBundle.createSslContext();

}
else {
SSLContext sslContext = SSLContext.getInstance(ssl.getAlgorithm());
KeyManager[] keyManagers = configureKeyManagers(ssl);
TrustManager[] trustManagers = configureTrustManagers(ssl);
sslContext.init(keyManagers, trustManagers, new SecureRandom());
return sslContext;
}
}

private KeyManager @Nullable [] configureKeyManagers(EmbeddedLdapProperties.Ssl ssl) throws KeyStoreException,
IOException, NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException {
String keyStoreName = ssl.getKeyStore();
String keyStorePassword = ssl.getKeyStorePassword();
String storeType = ssl.getKeyStoreType();
char[] keyPassphrase = null;
if (keyStorePassword != null) {
keyPassphrase = keyStorePassword.toCharArray();
}
KeyManager[] keyManagers = null;
if (StringUtils.hasText(keyStoreName)) {
Resource resource = this.resourceLoader.getResource(keyStoreName);
KeyStore ks = KeyStore.getInstance(storeType);
try (InputStream inputStream = resource.getInputStream()) {
ks.load(inputStream, keyPassphrase);
}
KeyManagerFactory kmf = KeyManagerFactory.getInstance(ssl.getKeyStoreAlgorithm());
kmf.init(ks, keyPassphrase);
keyManagers = kmf.getKeyManagers();
}
return keyManagers;
}

private TrustManager @Nullable [] configureTrustManagers(EmbeddedLdapProperties.Ssl ssl)
throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException {
String trustStoreName = ssl.getTrustStore();
String trustStorePassword = ssl.getTrustStorePassword();
String storeType = ssl.getTrustStoreType();
char[] trustPassphrase = null;
if (trustStorePassword != null) {
trustPassphrase = trustStorePassword.toCharArray();
}
TrustManager[] trustManagers = null;
if (StringUtils.hasText(trustStoreName)) {
Resource resource = this.resourceLoader.getResource(trustStoreName);
KeyStore tks = KeyStore.getInstance(storeType);
try (InputStream inputStream = resource.getInputStream()) {
tks.load(inputStream, trustPassphrase);
}
TrustManagerFactory tmf = TrustManagerFactory.getInstance(ssl.getTrustStoreAlgorithm());
tmf.init(tks);
trustManagers = tmf.getTrustManagers();
}
return trustManagers;
}

/**
* {@link SpringBootCondition} to determine when to apply embedded LDAP
* auto-configuration.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@

package org.springframework.boot.ldap.autoconfigure.embedded;

import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;

import javax.net.ssl.SSLContext;

import org.jspecify.annotations.Nullable;

import org.springframework.boot.context.properties.ConfigurationProperties;
Expand Down Expand Up @@ -62,6 +65,11 @@ public class EmbeddedLdapProperties {
*/
private final Validation validation = new Validation();

/**
* SSL configuration.
*/
private final Ssl ssl = new Ssl();

public int getPort() {
return this.port;
}
Expand Down Expand Up @@ -98,6 +106,10 @@ public Validation getValidation() {
return this.validation;
}

public Ssl getSsl() {
return this.ssl;
}

public static class Credential {

/**
Expand Down Expand Up @@ -132,6 +144,174 @@ boolean isAvailable() {

}

public static class Ssl {

private static final String SUN_X509 = "SunX509";

private static final String DEFAULT_PROTOCOL;

static {
String protocol = "TLSv1.1";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security-wise, TLS v3 should be the default and not v1 which is not really supported anymore.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default will be TLSv1.2 as long as the JDK supports it (see the try block below). AFAIK, there's no TLS v3. There is, however, TLSv1.3.

Copy link

@cdprete cdprete Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, with v3 I meant 1.3.

The default will be TLSv1.2 as long as the JDK supports it

Which I would argue, security-wise, why not v1.3 if available?
Personally, I think that the default should be the most secure supported option and the user must take an explicit risk by explicitly configure the application to something lower if he/she really wants to.

Also, although it would be weird, getProtocols() can actually return null which would lead to a NPE when the loop gets initiated. Dunno if you want to handle/check such weird case to begin with.

try {
String[] protocols = SSLContext.getDefault().getSupportedSSLParameters().getProtocols();
for (String prot : protocols) {
if ("TLSv1.2".equals(prot)) {
protocol = "TLSv1.2";
break;
}
}
}
catch (NoSuchAlgorithmException ex) {
// nothing
}
DEFAULT_PROTOCOL = protocol;
}

/**
* Whether to enable SSL support.
*/
private Boolean enabled = false;

/**
* SSL bundle name.
*/
private @Nullable String bundle;

/**
* Path to the key store that holds the SSL certificate.
*/
private @Nullable String keyStore;

/**
* Key store type.
*/
private String keyStoreType = "PKCS12";

/**
* Password used to access the key store.
*/
private @Nullable String keyStorePassword;

/**
* Key store algorithm.
*/
private String keyStoreAlgorithm = SUN_X509;

/**
* Trust store that holds SSL certificates.
*/
private @Nullable String trustStore;

/**
* Trust store type.
*/
private String trustStoreType = "JKS";

/**
* Password used to access the trust store.
*/
private @Nullable String trustStorePassword;

/**
* Trust store algorithm.
*/
private String trustStoreAlgorithm = SUN_X509;

/**
* SSL algorithm to use.
*/
private String algorithm = DEFAULT_PROTOCOL;

public Boolean isEnabled() {
return this.enabled;
}

public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}

public @Nullable String getBundle() {
return this.bundle;
}

public void setBundle(@Nullable String bundle) {
this.bundle = bundle;
}

public @Nullable String getKeyStore() {
return this.keyStore;
}

public void setKeyStore(@Nullable String keyStore) {
this.keyStore = keyStore;
}

public String getKeyStoreType() {
return this.keyStoreType;
}

public void setKeyStoreType(String keyStoreType) {
this.keyStoreType = keyStoreType;
}

public @Nullable String getKeyStorePassword() {
return this.keyStorePassword;
}

public void setKeyStorePassword(@Nullable String keyStorePassword) {
this.keyStorePassword = keyStorePassword;
}

public String getKeyStoreAlgorithm() {
return this.keyStoreAlgorithm;
}

public void setKeyStoreAlgorithm(String keyStoreAlgorithm) {
this.keyStoreAlgorithm = keyStoreAlgorithm;
}

public @Nullable String getTrustStore() {
return this.trustStore;
}

public void setTrustStore(@Nullable String trustStore) {
this.trustStore = trustStore;
}

public String getTrustStoreType() {
return this.trustStoreType;
}

public void setTrustStoreType(String trustStoreType) {
this.trustStoreType = trustStoreType;
}

public @Nullable String getTrustStorePassword() {
return this.trustStorePassword;
}

public void setTrustStorePassword(@Nullable String trustStorePassword) {
this.trustStorePassword = trustStorePassword;
}

public String getTrustStoreAlgorithm() {
return this.trustStoreAlgorithm;
}

public void setTrustStoreAlgorithm(String trustStoreAlgorithm) {
this.trustStoreAlgorithm = trustStoreAlgorithm;
}

public String getAlgorithm() {
return this.algorithm;
}

public void setAlgorithm(String sslAlgorithm) {
this.algorithm = sslAlgorithm;
}

}

public static class Validation {

/**
Expand Down
Loading