javaspring-bootactive-directoryldapspring-security-ldap

Ldap AD Authentication in Spring Security


I have a login page in my application where I want to validate the entered username/password against Ldap AD. I am thinking of creating a bind and get a context. If bind is successful that means user is authenticated. In Java I have achieved it like this:

public class Test {
    public static void main(String[] args) {
        try {

            Hashtable<String, String> env = new Hashtable<String, String>();
            env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
            env.put(Context.PROVIDER_URL, "ldap://ldapserver:389");
            env.put(Context.SECURITY_AUTHENTICATION, "simple");
            env.put(Context.SECURITY_PRINCIPAL, "domain\\userId");  //coming from frontend login form
            env.put(Context.SECURITY_CREDENTIALS, password); // from login form

            LdapContext ctx = new InitialLdapContext(env, null);
            ctx.setRequestControls(null);
            NamingEnumeration<?> namingEnum = ctx.search("ou=users,ou=in,dc=global,dc=company,dc=org", "(objectclass=user)", getSimpleSearchControls());
            for (int i=0;i<1;i++) {
                SearchResult result = (SearchResult) namingEnum.next();
                Attributes attrs = result.getAttributes();
                NamingEnumeration<String> nam =attrs.getIDs();
                while(nam.hasMore()) {
                    System.out.println(nam.next());
                }
            }
            namingEnum.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    private static SearchControls getSimpleSearchControls() {
        SearchControls searchControls = new SearchControls();
        searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
        searchControls.setTimeLimit(30000);
        //String[] attrIDs = {"objectGUID"};
        //searchControls.setReturningAttributes(attrIDs);
        return searchControls;
    }
}

Above code is perfectly working for me. I want to implement the same in spring boot application using spring security. I have tried multiple suggested ways but getting some error every time.

Here I want to authenticate the enter username/password for which I think context binding is enough so not require to search that user again using something like "sAMAccountName={0}" please correct me if I am wrong.


Update

while trying below code, I can see in the logs that it fetches the user details but in the last giving some error:

      auth
        .ldapAuthentication()
        .userSearchFilter("(sAMAccountName={0})")
        .userDnPatterns()
        .userSearchBase("dc=global,dc=company,dc=org")
        .contextSource()
        .url("ldap://ldapserver")
        .port(389)
        .managerDn("domain\\userId")
        .managerPassword("******");

Error Logs:

2022-08-04 19:47:44.477 TRACE 33541 --- [nio-8080-exec-2] o.s.s.w.a.www.BasicAuthenticationFilter  : Found username 'userId' in Basic Authorization header 
2022-08-04 19:47:44.478 TRACE 33541 --- [nio-8080-exec-2] o.s.s.authentication.ProviderManager     : Authenticating request with LdapAuthenticationProvider (1/1) 
2022-08-04 19:47:44.478 DEBUG 33541 --- [nio-8080-exec-2] o.s.s.l.a.LdapAuthenticationProvider     : Processing authentication request for user: userId 
2022-08-04 19:47:44.479 DEBUG 33541 --- [nio-8080-exec-2] o.s.s.l.s.FilterBasedLdapUserSearch      : Searching for user 'userId', with user search [ searchFilter: '(sAMAccountName={0})', searchBase: 'dc=global,dc=company,dc=org', scope: subtree, searchTimeLimit: 0, derefLinkFlag: false ] 
2022-08-04 19:47:44.831 DEBUG 33541 --- [nio-8080-exec-2] o.s.l.c.support.AbstractContextSource    : Got Ldap context on server 'ldap://ldapserver' 
2022-08-04 19:47:45.241 DEBUG 33541 --- [nio-8080-exec-2] o.s.s.ldap.SpringSecurityLdapTemplate    : Searching for entry under DN '', base = 'dc=global,dc=company,dc=org', filter = '(sAMAccountName={0})' 
2022-08-04 19:47:45.252 DEBUG 33541 --- [nio-8080-exec-2] o.s.s.ldap.SpringSecurityLdapTemplate    : Found DN: CN=Kumar\, Rajesh,OU=Users,OU=IN,DC=global,DC=company,DC=org 
2022-08-04 19:47:45.253  INFO 33541 --- [nio-8080-exec-2] o.s.s.ldap.SpringSecurityLdapTemplate    : Ignoring PartialResultException 
2022-08-04 19:47:45.254 DEBUG 33541 --- [nio-8080-exec-2] o.s.s.l.a.BindAuthenticator              : Attempting to bind as cn=Kumar\, Rajesh,ou=Users,ou=IN,dc=global,dc=company,dc=org 
2022-08-04 19:47:45.254 DEBUG 33541 --- [nio-8080-exec-2] s.s.l.DefaultSpringSecurityContextSource : Removing pooling flag for user cn=Kumar\, Rajesh,ou=Users,ou=IN,dc=global,dc=company,dc=org 
2022-08-04 19:47:45.622 DEBUG 33541 --- [nio-8080-exec-2] o.s.l.c.support.AbstractContextSource    : Got Ldap context on server 'ldap://ldapserver' 
2022-08-04 19:47:45.623 DEBUG 33541 --- [nio-8080-exec-2] o.s.s.l.a.BindAuthenticator              : Retrieving attributes... 2022-08-04 19:47:45.625 DEBUG 33541 --- [nio-8080-exec-2] .s.s.l.u.DefaultLdapAuthoritiesPopulator : Getting authorities for user cn=Kumar\, Rajesh,ou=Users,ou=IN,dc=global,dc=company,dc=org 
2022-08-04 19:47:45.627 DEBUG 33541 --- [nio-8080-exec-2] .s.s.l.u.DefaultLdapAuthoritiesPopulator : Searching for roles for user 'userId', DN = 'cn=Kumar\, Rajesh,ou=Users,ou=IN,dc=global,dc= company,dc=org', with filter (uniqueMember={0}) in search base '' 
2022-08-04 19:47:45.628 DEBUG 33541 --- [nio-8080-exec-2] o.s.s.ldap.SpringSecurityLdapTemplate    : Using filter: (uniqueMember=cn=Kumar\5c, Rajesh,ou=Users,ou=IN,dc=global,dc= company,dc=org) 
2022-08-04 19:47:45.628 DEBUG 33541 --- [nio-8080-exec-2] o.s.ldap.core.LdapTemplate               : The returnObjFlag of supplied SearchControls is not set but a ContextMapper is used - setting flag to true 
2022-08-04 19:47:45.629 DEBUG 33541 --- [nio-8080-exec-2] o.s.l.c.support.AbstractContextSource    : Got Ldap context on server 'ldap://ldapserver' 
2022-08-04 19:47:45.837 TRACE 33541 --- [nio-8080-exec-2] o.s.s.w.header.writers.HstsHeaderWriter  : Not injecting HSTS header since it did not match request to [Is Secure] 
2022-08-04 19:47:45.838 DEBUG 33541 --- [nio-8080-exec-2] w.c.HttpSessionSecurityContextRepository : Did not store empty SecurityContext 
2022-08-04 19:47:45.838 DEBUG 33541 --- [nio-8080-exec-2] s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request 
2022-08-04 19:47:45.839 TRACE 33541 --- [nio-8080-exec-2] o.s.b.w.s.f.OrderedRequestContextFilter  : Cleared thread-bound request context: org.apache.catalina.connector.RequestFacade@1c138f13 
2022-08-04 19:47:45.847 ERROR 33541 --- [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception
    
org.springframework.ldap.NameNotFoundException: [LDAP: error code 32 - 0000208D: NameErr: DSID-03100217, problem 2001 (NO_OBJECT), data 0, best match of:   '' ]; 
nested exception is javax.naming.NameNotFoundException: [LDAP: error code 32 - 0000208D: NameErr: DSID-03100217, problem 2001 (NO_OBJECT), data 0, best match of:   '' ]; remaining name ''     
at org.springframework.ldap.support.LdapUtils.convertLdapException(LdapUtils.java:183) ~[spring-ldap-core-2.3.4.RELEASE.jar:2.3.4.RELEASE]  
at org.springframework.ldap.core.LdapTemplate.search(LdapTemplate.java:376) ~[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]

enter image description here

Can someone please guide me to migrate this plain java code to spring security.

Regards


Solution

  • We can see in the trace logs the following message :

    DefaultLdapAuthoritiesPopulator : Searching for roles for user 'userId', DN = 'cn=Kumar\, Rajesh,ou=Users,ou=IN,dc=global,dc= company,dc=org', with filter (uniqueMember={0}) in search base ''
    

    The documentation says :

    After authenticating the user successfully, the LdapAuthenticationProvider will attempt to load a set of authorities for the user by calling the configured LdapAuthoritiesPopulator.

    The default implementation is trying to load the authorities by searching the directory for groups the user is a member of, but is failing to do so because you did not specify the group search base.

      auth
        .ldapAuthentication()
        .userSearchFilter("(sAMAccountName={0})")
        .userSearchBase("dc=global,dc=company,dc=org")
        .groupSearchBase("dc=global,dc=company,dc=org")  // <- here
        .contextSource()
        .url("ldap://ldapserver")
        .port(389)
        .managerDn("domain\\userId")
        .managerPassword("******");
    

    Usually, such groups are referenced under an ou=Roles component in the directory tree. For example, given the user search base defined in the "working" code : ou=roles,ou=in,dc=global,dc=company,dc=org, but a larger base (with only dc's) should be fine to start with.

    It is worth noting that you can set a global base directly in the ldap url, and define relative dn for parameters where a dn is expected (with a valid global base set, leaving groupSearchBase empty wouldn't throw an error), you could have for example :

        .userSearchBase("ou=users,ou=in")
        .groupSearchBase("ou=roles,ou=in")
    

    with :

        .url("ldap://ldapserver:389/dc=global,dc=company,dc=org")
    

    Also, note that you don't need userDnPatterns() when using userSearchFilter(), use either one or the other.

    @see Spring Security documentation : Loading Authorities