3
votes

I'm still learning LDAP / Active Directory so correct me if my terminology is wrong at all :)

In our Java Web Application, I'm trying to secure it with Spring Security LDAP. I managed to get Spring Security working with in-memory authentication but we need to tie it to our AD server.

I'm going to mask our actual domain with com.test

Here is the error I receive when I try to login from my application

13:39:55,701 ERROR ActiveDirectoryLdapAuthenticationProvider:133 - Failed to locate directory entry for authenticated user: johnsmit javax.naming.NameNotFoundException: [LDAP: error code 32 - 0000208D: NameErr: DSID-0310020A, problem 2001 (NO_OBJECT), data 0, best match of: 'CN=Users,DC=ad,DC=test,DC=com'

I am using class based configuration with Spring Here is my SecurityConfiguration class

@Configuration
@EnableWebMvcSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Bean
    public ActiveDirectoryLdapAuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
          provider = new ActiveDirectoryLdapAuthenticationProvider("ad.test.com", "ldap://servername.ad.test.com:389/cn=Users,dc=ad,dc=test,dc=com");

        provider.setUseAuthenticationRequestCredentials(true);


        return provider;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {

        auth.authenticationProvider(activeDirectoryLdapAuthenticationProvider());

    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.
                authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin().failureUrl("/login?error")
                .loginPage("/login")
                .permitAll()
                .and()
                .logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout")).logoutSuccessUrl("/login")
                .permitAll()
                .and()
                .httpBasic();

    }
}

So here is the issue (at least I think)... In our AD server we have our cn,name,sAMAccountName and uid as our username that we login with, johnsmit in my example above.

Our userPrincipalName (in our AD server) is our email address so [email protected].

I was looking at the ActiveDirectoryLdapAuthenticationProvider class and it says it uses the userPrincipalName. Looking in the code here on github it shows that it is using userPrincipalName. I checked the newer versions of Spring Security which is not General Availability yet, but it was the same thing.

There must be someway that I can search AD with the username "johnsmit" instead of "[email protected]"...

If the searchFilter was String searchFilter = "(&(objectClass=user)(sAMAccountName={0}))"; that would be the ideal situation but I don't know if that is possible to override anywhere and I can't find any documentation?

3

3 Answers

2
votes

As I found this eminently helpful in solving my own problem, I'd just like to add that the changes suggested by @JanTheGun appear to have been folded into Spring Security 5.0.8.RELEASE / Spring Boot 2.0.5

The JavaDoc for ActiveDirectoryLdapAuthenticationProvider.setSearchFilter(String) reads:

The LDAP filter string to search for the user being authenticated. Occurrences of {0} are replaced with the username@domain. Occurrences of {1} are replaced with the username only.

Defaults to: (&(objectClass=user)(userPrincipalName={0})))

Therefore, in Spring Security 5.0.8.RELEASE, it's possible to use the suggested search-filter change without having to replicate any of the Spring Security classes!

1
votes

I guess I will answer my own question with this Jira ticket

https://jira.spring.io/browse/SEC-1915

Looks like it simply hasn't been merged in. The patch in this ticket would be the answer I need though.

1
votes

Three years later I am also struggling with that problem. As there is the recommendation to change the userPrincipalName to the mail address for Office365 (see Office 365 – Why Your UPN Should Match Your Primary SMTP Address) I thought others might also have the problem - so here is my fix.

There is a discussion on github that is talking about the issue: https://github.com/spring-projects/spring-security/issues/2448

The answer of user gkibilov solved my problem. The idea is to change the "searchForUser" method in the ActiveDirectoryLdapAuthenticationProvider class in order to not only pass the bindprincipal (=USERNAME@DOMAIN) but also the username.

Afterwards the following searchFilter can be used to look for the sAMAccountName instead of the userPrincipalName:

"(&(objectClass=user)(sAMAccountName={1}))"

Here are the single steps that I did:

  1. Copy the source of the ActiveDirectoryAuthenticationException class to a new class and rename it (ie "MyActiveDirectoryAuthenticationException"): SourceLink

  2. Copy the source of the ActiveDirectoryLdapAuthenticationProvider class to a new class and rename it (ie "MyActiveDirectoryLdapAuthenticationProvider"): SourceLink

  3. Exchange the ActiveDirectoryAuthenticationException class in MyActiveDirectoryLdapAuthenticationProvider with the new exception class (MyActiveDirectoryAuthenticationException)

  4. Change the method in MyActiveDirectoryAuthenticationException class to also pass the username:

    private DirContextOperations searchForUser(DirContext context, String username)
        throws NamingException {
         SearchControls searchControls = new SearchControls();
         searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
    
         String bindPrincipal = createBindPrincipal(username);
         String searchRoot = rootDn != null ? rootDn
                 : searchRootFromPrincipal(bindPrincipal);
    
         try {
             return SpringSecurityLdapTemplate.searchForSingleEntryInternal(context,
                searchControls, searchRoot, searchFilter,
                new Object[] { bindPrincipal, username});
         }
         ...
    }
    
  5. Change the searchFilter to look for the sAMAccountName attribute. In your case the bean should look like this:

    @Bean
    public MyActiveDirectoryLdapAuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
        provider = new MyActiveDirectoryLdapAuthenticationProvider("ad.test.com", "ldap://servername.ad.test.com:389/cn=Users,dc=ad,dc=test,dc=com");
        provider.setSearchFilter("(&(objectClass=user)(sAMAccountName={1}))");
        provider.setUseAuthenticationRequestCredentials(true);
        return provider;
    }
    

I have to admit that it is not the nicest solution to copy a class but as the original class is "final" I couldn't find a better way to fix this problem.