spring-bootspring-securityactive-directoryspring-security-ldapldapauth

How to authenticate a ldap user from any ldap server (except embedded server) using bind authentication mechanism in spring security


I want to implement ldap authentication in an existing spring boot project for my company. So for that I am trying to implement one sample spring boot app first, in which I will authenticate a ldap user from my company ldap server. If it works, fine then I will implement the same code into my existing project and I want to achieve this using ldap bind authentication mechanism only.

Following is the code to authenticate a ldap user using bind authentication mechanism in spring security that I have written for my sample app:

Here is the server structure for user, from this image you will get idea what are attribute available for an user

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.6</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>SampleApp2</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>
    <name>SampleApp2</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
                <dependency>
                    <groupId>org.springframework.security</groupId>
                    <artifactId>spring-security-ldap</artifactId>
                        <version>5.5.3</version>
                        <type>jar</type>
                </dependency>
     <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-config</artifactId>
      <type>jar</type>
     </dependency>
         <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.ldap</groupId>
            <artifactId>spring-ldap-core</artifactId>
            <version>2.3.4.RELEASE</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

HomeController.java

package com.example.SampleApp2;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HomeController {

  @GetMapping("/")
  public String index() {
    return "Welcome to the home page!";
  }

}

WebSecurityConfig.java

package com.example.SampleApp2;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    public XyzEncryption xyzEncrption() { // custom password encoder which is used in company's ldap server to authenticate user, even though I didn't use it any where
    
        return new XyzEncryption();
    }
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http
      .authorizeRequests()
        .anyRequest().fullyAuthenticated()
        .and()
      .formLogin();
  }
  @Override
  public void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth
    .ldapAuthentication()     
    .contextSource()
    .url("ldap://in.xyz.com:389/DC=in,DC=xyz,DC=com")
    .managerDn("CN=Rohit Sarkar,OU=Engg,OU=KSPL Users,DC=in,DC=xyz,DC=com") //User Dn by which I am binding the connection with server
    .managerPassword("PasswordOfRohitSarkar")
    .and()
    .userSearchFilter("sAMAccountName=abdulg")
    .userDnPatterns("CN=Abdul Gaffar,OU=Engg,OU=KSPL Users,DC=in,DC=xyz,DC=com"); // User dn which need to be authenticated from server      
  }
}

I am getting this kind of default login page from spring security and loging with abdulg and it's password

But getting error:

org.springframework.ldap.PartialResultException: Unprocessed Continuation Reference(s); nested exception is javax.naming.PartialResultException: Unprocessed Continuation Reference(s); remaining name ''
    at org.springframework.ldap.support.LdapUtils.convertLdapException(LdapUtils.java:216) ~[spring-ldap-core-2.3.4.RELEASE.jar:2.3.4.RELEASE]
    at org.springframework.ldap.core.LdapTemplate.search(LdapTemplate.java:385) ~[spring-ldap-core-2.3.4.RELEASE.jar:2.3.4.RELEASE]
    at org.springframework.ldap.core.LdapTemplate.search(LdapTemplate.java:328) ~[spring-ldap-core-2.3.4.RELEASE.jar:2.3.4.RELEASE]
    at org.springframework.ldap.core.LdapTemplate.search(LdapTemplate.java:629) ~[spring-ldap-core-2.3.4.RELEASE.jar:2.3.4.RELEASE]
    at org.springframework.ldap.core.LdapTemplate.search(LdapTemplate.java:570) ~[spring-ldap-core-2.3.4.RELEASE.jar:2.3.4.RELEASE]
    at org.springframework.security.ldap.SpringSecurityLdapTemplate.searchForMultipleAttributeValues(SpringSecurityLdapTemplate.java:197) ~[spring-security-ldap-5.5.3.jar:5.5.3]
    at org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator.getGroupMembershipRoles(DefaultLdapAuthoritiesPopulator.java:223) ~[spring-security-ldap-5.5.3.jar:5.5.3]
    at org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator.getGrantedAuthorities(DefaultLdapAuthoritiesPopulator.java:203) ~[spring-security-ldap-5.5.3.jar:5.5.3]
    at org.springframework.security.ldap.authentication.LdapAuthenticationProvider.loadUserAuthorities(LdapAuthenticationProvider.java:197) ~[spring-security-ldap-5.5.3.jar:5.5.3]
    at org.springframework.security.ldap.authentication.AbstractLdapAuthenticationProvider.authenticate(AbstractLdapAuthenticationProvider.java:83) ~[spring-security-ldap-5.5.3.jar:5.5.3]
    at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:182) ~[spring-security-core-5.5.3.jar:5.5.3]
    at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:201) ~[spring-security-core-5.5.3.jar:5.5.3]
    at org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.attemptAuthentication(UsernamePasswordAuthenticationFilter.java:85) ~[spring-security-web-5.5.3.jar:5.5.3]
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:222) ~[spring-security-web-5.5.3.jar:5.5.3]
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:212) ~[spring-security-web-5.5.3.jar:5.5.3]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.5.3.jar:5.5.3]
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:103) ~[spring-security-web-5.5.3.jar:5.5.3]
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:89) ~[spring-security-web-5.5.3.jar:5.5.3]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.5.3.jar:5.5.3]
    at org.springframework.security.web.csrf.CsrfFilter.doFilterInternal(CsrfFilter.java:132) ~[spring-security-web-5.5.3.jar:5.5.3]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.3.12.jar:5.3.12]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.5.3.jar:5.5.3]
    at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:90) ~[spring-security-web-5.5.3.jar:5.5.3]
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:75) ~[spring-security-web-5.5.3.jar:5.5.3]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.3.12.jar:5.3.12]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.5.3.jar:5.5.3]
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:110) ~[spring-security-web-5.5.3.jar:5.5.3]
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:80) ~[spring-security-web-5.5.3.jar:5.5.3]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.5.3.jar:5.5.3]
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:55) ~[spring-security-web-5.5.3.jar:5.5.3]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.3.12.jar:5.3.12]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.5.3.jar:5.5.3]
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:211) ~[spring-security-web-5.5.3.jar:5.5.3]
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:183) ~[spring-security-web-5.5.3.jar:5.5.3]
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:358) ~[spring-web-5.3.12.jar:5.3.12]
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:271) ~[spring-web-5.3.12.jar:5.3.12]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [catalina.jar:8.5.70]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [catalina.jar:8.5.70]
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.3.12.jar:5.3.12]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.3.12.jar:5.3.12]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [catalina.jar:8.5.70]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [catalina.jar:8.5.70]
    at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.3.12.jar:5.3.12]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.3.12.jar:5.3.12]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [catalina.jar:8.5.70]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [catalina.jar:8.5.70]
    at org.springframework.boot.web.servlet.support.ErrorPageFilter.doFilter(ErrorPageFilter.java:126) [spring-boot-2.5.6.jar:2.5.6]
    at org.springframework.boot.web.servlet.support.ErrorPageFilter.access$000(ErrorPageFilter.java:64) [spring-boot-2.5.6.jar:2.5.6]
    at org.springframework.boot.web.servlet.support.ErrorPageFilter$1.doFilterInternal(ErrorPageFilter.java:101) [spring-boot-2.5.6.jar:2.5.6]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.3.12.jar:5.3.12]
    at org.springframework.boot.web.servlet.support.ErrorPageFilter.doFilter(ErrorPageFilter.java:119) [spring-boot-2.5.6.jar:2.5.6]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [catalina.jar:8.5.70]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [catalina.jar:8.5.70]
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) [spring-web-5.3.12.jar:5.3.12]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.3.12.jar:5.3.12]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [catalina.jar:8.5.70]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [catalina.jar:8.5.70]
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:196) [catalina.jar:8.5.70]
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) [catalina.jar:8.5.70]
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:544) [catalina.jar:8.5.70]
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135) [catalina.jar:8.5.70]
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:81) [catalina.jar:8.5.70]
    at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:698) [catalina.jar:8.5.70]
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) [catalina.jar:8.5.70]
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:364) [catalina.jar:8.5.70]
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:624) [tomcat-coyote.jar:8.5.70]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) [tomcat-coyote.jar:8.5.70]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:831) [tomcat-coyote.jar:8.5.70]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1650) [tomcat-coyote.jar:8.5.70]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-coyote.jar:8.5.70]
    at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) [tomcat-util.jar:8.5.70]
    at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) [tomcat-util.jar:8.5.70]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-util.jar:8.5.70]
    at java.lang.Thread.run(Thread.java:748) [na:1.8.0_302]
Caused by: javax.naming.PartialResultException: Unprocessed Continuation Reference(s)
    at com.sun.jndi.ldap.LdapCtx.processReturnCode(LdapCtx.java:3024) ~[na:1.8.0_302]
    at com.sun.jndi.ldap.LdapCtx.processReturnCode(LdapCtx.java:2998) ~[na:1.8.0_302]
    at com.sun.jndi.ldap.LdapCtx.searchAux(LdapCtx.java:1874) ~[na:1.8.0_302]
    at com.sun.jndi.ldap.LdapCtx.c_search(LdapCtx.java:1797) ~[na:1.8.0_302]
    at com.sun.jndi.toolkit.ctx.ComponentDirContext.p_search(ComponentDirContext.java:392) ~[na:1.8.0_302]
    at com.sun.jndi.toolkit.ctx.PartialCompositeDirContext.search(PartialCompositeDirContext.java:358) ~[na:1.8.0_302]
    at com.sun.jndi.toolkit.ctx.PartialCompositeDirContext.search(PartialCompositeDirContext.java:341) ~[na:1.8.0_302]
    at javax.naming.directory.InitialDirContext.search(InitialDirContext.java:267) ~[na:1.8.0_302]
    at org.springframework.ldap.core.LdapTemplate$4.executeSearch(LdapTemplate.java:322) ~[spring-ldap-core-2.3.4.RELEASE.jar:2.3.4.RELEASE]
    at org.springframework.ldap.core.LdapTemplate.search(LdapTemplate.java:363) ~[spring-ldap-core-2.3.4.RELEASE.jar:2.3.4.RELEASE]
    ... 72 common frames omitted

I have used follwoing instead of hard coding:

.userSearchFilter("sAMAccountName={0}")

Still getting same error.

I have tried ad authentication provider to authenticate the user and its being authenticated successfully, but since it's not a standard way in spring security, I want to use bind authentication and don't want to use password compare authentication. I want to keep user's password hidden.

Following is the code to authenticate user using ad authentication provider:

package com.example.SampleApp2;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    public KovairEncryption kovairEncrption() {
    
        return new KovairEncryption();
    }
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http
      .authorizeRequests()
        .anyRequest().fullyAuthenticated()
        .and()
      .formLogin();
  }

  @Override
  public void configure(AuthenticationManagerBuilder auth) throws Exception {
  
    auth.authenticationProvider(activeDirectoryLdapAuthenticationProvider());
  }
  
    @Bean
    public AuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
        ActiveDirectoryLdapAuthenticationProvider authenticationProvider =
        new ActiveDirectoryLdapAuthenticationProvider("in.xyz.com", "ldap://in.xyz.com")
        authenticationProvider.setConvertSubErrorCodesToExceptions(true);
        authenticationProvider.setUseAuthenticationRequestCredentials(true);
        return authenticationProvider;
    }
}

I have also tried traditional java code to authenticate ad user it was also working fine but unable to authenticate using bind authentication mechanism. I have read lot of articles and have gone through lot of spring reference document but couldn't find any solution. I have already spent almost one week to get a proper solution.


Solution

  • OK. So after spending lot of times I got solutions.

    First of all, there is nothing wrong with the code snippet mentioned above, reason of this error is-

    Accroding to spring security doc under section 18.4.4, In case of active directory search, after authenticating the user successfully, the LdapAuthenticationProvider will attempt to load a set of authorities for the user by calling the configured LdapAuthoritiesPopulator bean. The DefaultLdapAuthoritiesPopulator is an implementation which will load the authorities by searching the directory for groups of which the user is a member (typically these will be groupOfNames or groupOfUniqueNames entries in the directory).

    If you want to use LDAP only for authentication, but load the authorities from a difference source (such as a database) then you can provide your own implementation of this interface and inject that instead.

    But I was ignoring the authorities which was returning after successfull authentication of the user, that is why I was getting this error.

    There are few ways to solve the problem:

    1st Way: using Context.REFERRAL to follow

    Disadvantage: it will take lot of time to follow the referral and authenticate user

    2nd Way: By implementing LdapAuthoritiesPopulator

    CustomAuthoritiesPopulator.java

    @Component
    public class CustomAuthoritiesPopulator implements LdapAuthoritiesPopulator {
    
        @Override
        public Collection<? extends GrantedAuthority> getGrantedAuthorities(DirContextOperations dco, String string) {
            Set<GrantedAuthority> grantedAuthorities = new HashSet<>();
            grantedAuthorities.add(new SimpleGrantedAuthority("ADMIN"));
            return grantedAuthorities;
        }
        
    }
    

    WebSecurityConfig.java

    @Autowired
    CustomAuthoritiesPopulator authoritiespopulator;
    
    // I have mentioned only configure() method here, others code will be as it is
    
    @Override
      public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
        .ldapAuthentication()     
        .contextSource()
        .url("ServerUrl")
        .managerDn("BindUserDN") 
        .managerPassword("BindUserPassword")
        .and()
        .ldapAuthoritiesPopulator(authoritiespopulator)
        .userSearchFilter("sAMAccountName=UserName")
        
      }
    
    

    Note: This will be helpful if you want set some custom authority

    3rd Way: Using .setIgnorePartialResultException(true)

    WebSecurityConfig.java

    @Bean
    LdapContextSource ldapContextSource() {
       LdapContextSource ldapContextSource = new LdapContextSource();
       ldapContextSource.setUrl("ServerUrl");
       ldapContextSource.setUserDn("BindUserDN");
       dapContextSource.setPassword("BindUserPassword");
       ldapContextSource.afterPropertiesSet();
    }
    
    @Bean
    public LdapAuthoritiesPopulator ldapAuthoritiesPopulator() throws Exception { 
    DefaultLdapAuthoritiesPopulator authoritiespopulator= new DefaultLdapAuthoritiesPopulator(ldapContextSource(), "base");
            authoritiespopulator.setIgnorePartialResultException(true);
            return authoritiespopulator;
        }
    
    @Override
      public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
        .ldapAuthentication()     
        .contextSource()
        .url("ServerUrl")
        .managerDn("BindUserDN") 
        .managerPassword("BindUserPassword")
        .and()
        .ldapAuthoritiesPopulator(authoritiespopulator)
        .userSearchFilter("sAMAccountName=UserName")
        
      }