Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using custom truststore more complicated than necessary #494

Closed
marschall opened this issue Oct 16, 2018 · 14 comments
Closed

Using custom truststore more complicated than necessary #494

marschall opened this issue Oct 16, 2018 · 14 comments
Assignees
Labels
status: duplicate A duplicate of another issue

Comments

@marschall
Copy link

We want to use Spring LDAP with a custom truststore because we want to use LDAPS and truststore with only the CA certificate of the LDAP servers. We do not want to configure a truststore globally, we are using and following LDAP referrals.

We were initially using a DefaultTlsDirContextAuthenticationStrategy and configuring the sslSocketFactory property. We found that was not enough. We only got it working by setting the java.naming.ldap.factory.socket property. This required us to:

  • implement a custom SSLSocketFactory
  • implement a custom SimpleDirContextAuthenticationStrategy that sets the java.naming.ldap.factory.socket property in the #setupEnvironment(Hashtable, String, String) method.
  • implement a custom DefaultSpringSecurityContextSource that in sets the java.naming.ldap.factory.socket property in the #getAuthenticatedEnv(String, String) method
@rwinch
Copy link
Member

rwinch commented Oct 16, 2018

Thanks for the feedback. What did you have in mind as a solution? As far as I know this is the Java supported way to customize the truststore and there isn't a lot we can do when using the JDK as the underlying client.

@marschall
Copy link
Author

I think a couple of things can be done. I order of increasing complexity

  1. Document what people have to do if they want to achieve what we did.
  2. Enhance the respective classes so that they accept an SSLSocketFactory
  3. Provide an SSLSocketFactoryFactoryBean that takes
    • a truststore location
    • a truststore type
    • a truststore passphrase
    • a keystrore location, optional
    • a keystrore type, optional
    • a keystrore passphrase, optional
    • a list of cipher suites, optional
      that then creates an SSLSocketFactory implementation class. This would require dynamically generating an SSLSocketFactory subclass but would offer the most convent interface for clients.

@rwinch
Copy link
Member

rwinch commented Oct 17, 2018

Thanks for your thoughts. Would you create separate tickets for each of these and link back to this issue? Any interested in submitting PRs for the features?

@AvinashKondakalla
Copy link

Any updates on this thread?

@satyy
Copy link

satyy commented Jan 7, 2020

Any update on this thread?? is there any other possible way to use SPRING LDAP with custom truststore?
If not, is there any example on how to configure spring ldap to support LDAPS with the current implementation ??

@shark300
Copy link

shark300 commented Jan 7, 2020

If not, is there any example on how to configure spring ldap to support LDAPS with the current implementation ??

public class TruststoreDirContextAuthenticationStrategy
    extends SimpleDirContextAuthenticationStrategy {
  @Override
  public void setupEnvironment(Hashtable<String, Object> env, String userDn, String password) {
    super.setupEnvironment(env, userDn, password);
    env.put("java.naming.ldap.factory.socket", TruststoreSSLSocketFactoryHolder.class.getName());
  }
}

public class TruststoreSSLSocketFactoryHolder implements InitializingBean {

  private static SocketFactory instance;
  private final SSLContextProvider sslContextProvider;

  public TruststoreSSLSocketFactoryHolder(SSLContextProvider sslContextProvider) {
    this.sslContextProvider = sslContextProvider;
  }

  @SuppressWarnings("squid:S2696")
  @Override
  public void afterPropertiesSet() {
    TruststoreSSLSocketFactoryHolder.instance = new TruststoreSSLSocketFactory(sslContextProvider);
  }

  public static SocketFactory getDefault() {
    return instance;
  }
}

public class TruststoreSSLSocketFactory extends SSLSocketFactory {
  private final SSLSocketFactory socketFactory;

  TruststoreSSLSocketFactory(SSLContextProvider contextProvider) {
    this.socketFactory = contextProvider.get().getSocketFactory();
  }

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

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

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

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

  @Override
  public Socket createSocket(String host, int port, InetAddress clientAddress, int clientPort)
      throws IOException {
    return socketFactory.createSocket(host, port, clientAddress, clientPort);
  }

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

  @Override
  public Socket createSocket(
      InetAddress address, int port, InetAddress clientAddress, int clientPort) throws IOException {
    return socketFactory.createSocket(address, port, clientAddress, clientPort);
  }

  @Override
  public Socket createSocket(Socket s, InputStream consumed, boolean autoClose) throws IOException {
    return socketFactory.createSocket(s, consumed, autoClose);
  }

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

public class SSLContextProvider implements Supplier<SSLContext> {

  private final TrustManager[] trustManagers;

  public SSLContextProvider(TrustManager[] trustManagers) {
    this.trustManagers = trustManagers;
  }

  public SSLContext get() {
    try {
      SSLContext ssl = SSLContext.getInstance("TLSv1.2");
      ssl.init(null, trustManagers, null);
      return ssl;
    } catch (NoSuchAlgorithmException | KeyManagementException e) {
      throw new IllegalStateException(e);
    }
  }
}

@Configuration
public class SSLSocketFactoryConfiguration {

  @Bean
  public TruststoreSSLSocketFactoryHolder truststoreSSLSocketFactoryHolder(
      TrustManager[] trustManagers) {
    return new TruststoreSSLSocketFactoryHolder(buildSslContextProvider(trustManagers));
  }

  private SSLContextProvider buildSslContextProvider(TrustManager[] trustManagers) {
    return new SSLContextProvider(trustManagers);
  }
}

public class MyLdapContextSource extends LdapContextSource {

  @Override
  public void afterPropertiesSet() {
    super.afterPropertiesSet();
    var urls = getUrls();
    Stream.of(urls)
      .filter(isSecured())
      .findAny()
      .ifPresent(ignored ->
       setAuthenticationStrategy(new TruststoreDirContextAuthenticationStrategy()));
  }

  private static Predicate<String> isSecured() {
    return url -> url.startsWith("ldaps://")
  }
}

Unfortunately, it has a dependency for org.springframework.ws.soap.security.support.TrustManagersFactoryBean (trustManagers) in our applications but we are using SOAP too.

@marschall
Copy link
Author

I created the following 3 follow up issues:

@satyy
Copy link

satyy commented Jan 8, 2020

@marschall i have tried the code which you shared. But i am getting an below exception when i am trying to login(autheticate user from ldap server)

2020-01-08 19:49:19.792 ERROR 44 --- [nio-8888-exec-3] w.a.UsernamePasswordAuthenticationFilter : An internal error occurred while trying to authenticate the user.

org.springframework.security.authentication.InternalAuthenticationServiceException: simple bind failed: {service}:636; nested exception is javax.naming.CommunicationException: simple bind failed: {serviceName}:636 [Root exception is java.net.SocketException: Connection or outbound has closed]
at org.springframework.security.ldap.authentication.LdapAuthenticationProvider.doAuthentication(LdapAuthenticationProvider.java:206) ~[spring-security-ldap-4.2.4.RELEASE.jar!/:4.2.4.RELEASE]
at org.springframework.security.ldap.authentication.AbstractLdapAuthenticationProvider.authenticate(AbstractLdapAuthenticationProvider.java:85) ~[spring-security-ldap-4.2.4.RELEASE.jar!/:4.2.4.RELEASE]

Could you please help me on this?

@marschall
Copy link
Author

@satyy I would need a full stack trace and also debug logs with -Djavax.net.debug=ssl:handshake

@satyy
Copy link

satyy commented Jan 10, 2020

@marschall The issue is resolved. There was some issue with the tls protocol i was using. After, correcting it, now i am able to connect to server over ldaps with custom truststore using code which you shared.

@marschall @shark300 thanks for the help.

@rwinch rwinch added status: waiting-for-feedback We need additional information before we can continue and removed waiting-for-feedback labels May 4, 2020
@deepd

This comment has been minimized.

@andrewbrock-sahmri
Copy link

With thanks to the examples in this ticket, and in case it helps anyone, this is what I ended up doing. I'm using the excellent sslcontext-kickstart library which lets you use multiple sources for your truststores (e.g. JDK + one from the local filesystem + one from the classpath, etc.).

TruststoreDirContextAuthenticationStrategy

import org.springframework.ldap.core.support.SimpleDirContextAuthenticationStrategy;

import java.util.Hashtable;

public class TruststoreDirContextAuthenticationStrategy extends SimpleDirContextAuthenticationStrategy {
  @Override
  public void setupEnvironment(Hashtable<String, Object> env, String userDn, String password) {
    super.setupEnvironment(env, userDn, password);
    env.put("java.naming.ldap.factory.socket", TruststoreSSLSocketFactory.class.getName());
  }
}

TruststoreSSLSocketFactory

import nl.altindag.ssl.SSLFactory;

import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;

public class TruststoreSSLSocketFactory extends SSLSocketFactory {

  private final SSLSocketFactory delegate;

  public TruststoreSSLSocketFactory() {
    this.delegate = SSLFactory.builder()
      .withSystemTrustMaterial()
      .withDefaultTrustMaterial()
      .withTrustMaterial("truststore.jks", "changeit".toCharArray())
      .build().getSslSocketFactory();
  }

  @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(InetAddress host, int port) throws IOException {
    return delegate.createSocket(host, port);
  }

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

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

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

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

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

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

  @Override
  public Socket createSocket(Socket s, InputStream consumed, boolean autoClose) throws IOException {
    return delegate.createSocket(s, consumed, autoClose);
  }

  public static SocketFactory getDefault() {
    return new TruststoreSSLSocketFactory();
  }
}

I then have a class annotated with @Configuration and @EnableWebSecurity with the following code:

  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    if (url != null && bindUser != null && bindPassword != null) {
      LdapContextSource contextSource = new LdapContextSource();
      contextSource.setUrl(url);
      contextSource.setUserDn(bindUser);
      contextSource.setPassword(bindPassword);

      if (new URI(url).getScheme().equalsIgnoreCase("ldaps")) {
        contextSource.setAuthenticationStrategy(new TruststoreDirContextAuthenticationStrategy());
      }
      contextSource.afterPropertiesSet();

      auth.ldapAuthentication()
        .userDetailsContextMapper(userDetailsContextMapper())
        .userSearchFilter(userSearchFilter)
        .userSearchBase(userSearchBase)
        .contextSource(contextSource)
        .groupSearchFilter(groupSearchFilter)
        .groupSearchBase(groupSearchBase);
    }
  }

@rwinch rwinch self-assigned this Jan 25, 2021
@rwinch rwinch added status: duplicate A duplicate of another issue and removed status: waiting-for-feedback We need additional information before we can continue labels Jan 25, 2021
@rwinch
Copy link
Member

rwinch commented Jan 25, 2021

Closing in favor of the issues in #494 (comment)

@rwinch rwinch closed this as completed Jan 25, 2021
@twwolff
Copy link

twwolff commented Jun 18, 2022

If not, is there any example on how to configure spring ldap to support LDAPS with the current implementation ??

public class TruststoreDirContextAuthenticationStrategy
    extends SimpleDirContextAuthenticationStrategy {
  @Override
  public void setupEnvironment(Hashtable<String, Object> env, String userDn, String password) {
    super.setupEnvironment(env, userDn, password);
    env.put("java.naming.ldap.factory.socket", TruststoreSSLSocketFactoryHolder.class.getName());
  }
}

public class TruststoreSSLSocketFactoryHolder implements InitializingBean {

  private static SocketFactory instance;
  private final SSLContextProvider sslContextProvider;

  public TruststoreSSLSocketFactoryHolder(SSLContextProvider sslContextProvider) {
    this.sslContextProvider = sslContextProvider;
  }

  @SuppressWarnings("squid:S2696")
  @Override
  public void afterPropertiesSet() {
    TruststoreSSLSocketFactoryHolder.instance = new TruststoreSSLSocketFactory(sslContextProvider);
  }

  public static SocketFactory getDefault() {
    return instance;
  }
}

public class TruststoreSSLSocketFactory extends SSLSocketFactory {
  private final SSLSocketFactory socketFactory;

  TruststoreSSLSocketFactory(SSLContextProvider contextProvider) {
    this.socketFactory = contextProvider.get().getSocketFactory();
  }

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

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

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

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

  @Override
  public Socket createSocket(String host, int port, InetAddress clientAddress, int clientPort)
      throws IOException {
    return socketFactory.createSocket(host, port, clientAddress, clientPort);
  }

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

  @Override
  public Socket createSocket(
      InetAddress address, int port, InetAddress clientAddress, int clientPort) throws IOException {
    return socketFactory.createSocket(address, port, clientAddress, clientPort);
  }

  @Override
  public Socket createSocket(Socket s, InputStream consumed, boolean autoClose) throws IOException {
    return socketFactory.createSocket(s, consumed, autoClose);
  }

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

public class SSLContextProvider implements Supplier<SSLContext> {

  private final TrustManager[] trustManagers;

  public SSLContextProvider(TrustManager[] trustManagers) {
    this.trustManagers = trustManagers;
  }

  public SSLContext get() {
    try {
      SSLContext ssl = SSLContext.getInstance("TLSv1.2");
      ssl.init(null, trustManagers, null);
      return ssl;
    } catch (NoSuchAlgorithmException | KeyManagementException e) {
      throw new IllegalStateException(e);
    }
  }
}

@Configuration
public class SSLSocketFactoryConfiguration {

  @Bean
  public TruststoreSSLSocketFactoryHolder truststoreSSLSocketFactoryHolder(
      TrustManager[] trustManagers) {
    return new TruststoreSSLSocketFactoryHolder(buildSslContextProvider(trustManagers));
  }

  private SSLContextProvider buildSslContextProvider(TrustManager[] trustManagers) {
    return new SSLContextProvider(trustManagers);
  }
}

public class MyLdapContextSource extends LdapContextSource {

  @Override
  public void afterPropertiesSet() {
    super.afterPropertiesSet();
    var urls = getUrls();
    Stream.of(urls)
      .filter(isSecured())
      .findAny()
      .ifPresent(ignored ->
       setAuthenticationStrategy(new TruststoreDirContextAuthenticationStrategy()));
  }

  private static Predicate<String> isSecured() {
    return url -> url.startsWith("ldaps://")
  }
}

Unfortunately, it has a dependency for org.springframework.ws.soap.security.support.TrustManagersFactoryBean (trustManagers) in our applications but we are using SOAP too.

Hello i want to use a separate truststore for the ldap connection, i have take your example class, i don't understand why it is not working , all the class are called by spring but the setupenvironement function in class TruststoreDirContextAuthenticationStrategy is never called ( but the constructor is called ). I am using spring boot 2.5.4. Thank for any help.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: duplicate A duplicate of another issue
Projects
None yet
Development

No branches or pull requests

8 participants