Spring Security Ldap authentication userDn and password from login form

Viewed 10555

I'am trying to implement a Spring Security LDAP authentication using WebSecurityConfigurerAdapter.

So far it works fine, but the problem in my case is that I don't want the username and password of context to be hard coded. It must be the login and password of the user, so my question is how can I build the context and setting of the username and password from the login form?

This is the code I'm working with:

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

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

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .ldapAuthentication()
                .userSearchFilter("(sAMAccountName={0})")
                .contextSource(contextSource());
    }

    @Bean
    public BaseLdapPathContextSource contextSource() {
        LdapContextSource bean = new LdapContextSource();
        bean.setUrl("ldap://10.10.10.10:389");
        bean.setBase("DC=myDomaine,DC=com");
        //instead of this i want to put here the username and password provided by the user
        bean.setUserDn("myDomaine\\username");
        bean.setPassword("password");
        bean.setPooled(true);
        bean.setReferral("follow");
        bean.afterPropertiesSet();
        return bean;
    }
}

Thank you!

4 Answers

the userDN and password parameter in contextSource is a required parameter. It is like admin username and password for you to be able to acquire or create initial connection to the ldap server.

For you to be able to authenticate the username and password from login form. You can use the ldapTemplate:

        @Bean
    public BaseLdapPathContextSource contextSource() {
        LdapContextSource bean = new LdapContextSource();
        bean.setUrl("ldap://10.10.10.10:389");
        bean.setBase("DC=myDomaine,DC=com");
        //instead of this i want to put here the username and password provided by the user
        bean.setUserDn("myDomaine\\username");
        bean.setPassword("password");
        bean.setPooled(true);
        bean.setReferral("follow");
        bean.afterPropertiesSet();
        return bean;
    }



  @Bean
  public LdapTemplate ldapTemplate() {
      LdapTemplate template = new LdapTemplate(contextSource());

      return template;
  }

Then use this in your service class implementation:

@Service
public class LdapUserServiceImpl implements LdapUserService, BaseLdapNameAware {
    @Autowired
    protected ContextSource contextSource;
    @Autowired
    protected LdapTemplate ldapTemplate;

    @Override
    public boolean authenticate(String userDn, String credentials) {
        AndFilter filter = new AndFilter();
        filter.and(new EqualsFilter("sAMAccountName", userDn));

        return ldapTemplate.authenticate("", filter.toString(), credentials);
    }

}

Then call this service passing the username and password from login form like this:

boolean isAuthenticated = ldapUserService.authenticate(loginForm.getUsername(), loginForm.getPassword());

You can use a customized Authentication Provider to do the authentication.

in your security Configuration, you can auto wire a CustomAuthenticationProvider, which can get the username and password from login form:


@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomAuthenticationProvider customAuthProvider;

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http
                .authorizeRequests()
                .anyRequest()
                .authenticated()
                .and()
                .formLogin();
    }

    @Override
    public void configure(AuthenticationManagerBuilder auth)  {
        auth.authenticationProvider(customAuthProvider);
    }
}

And Now just implement the CustomAuthenticationProvider

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {

    private static final Logger LOG = LoggerFactory.getLogger(CustomAuthenticationProvider.class);

    @Value("${ldap.host}")
    String ldapHost;

    @Value("${ldap.port}")
    String ldapPort;

    @Value("${ldap.base-dn}")
    String baseDomainName;

    @Value("${ldap.domain-prefix}")
    String domainPrefix;

    @Value("${ldap.read.timeout}")
    String timeout;

    private static final String DEFAULT_JNDI_CONTEXT_FACTORY = "com.sun.jndi.ldap.LdapCtxFactory";


    @Override
    public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {

        //user and password are from user's input from login form...
        String user= authentication.getName();
        String password = authentication.getCredentials().toString();

        try {
            Set<String> roles = authenticate(user, password);

           if (CollectionUtils.isEmpty(roles))
                return null;

            List<GrantedAuthority> authorityList = new ArrayList<>();

            for(String role: roles){
                authorityList.add(new SimpleGrantedAuthority(role));
            }

            return new UsernamePasswordAuthenticationToken(user, password, authorityList);

        } catch (NamingException ex) {
            LOG.info("Naming Exception",ex);

        }
        return null;
    }

    public Set<String> authenticate(final String username, final String password) throws NamingException {

        InitialLdapContext ctx = null;
        NamingEnumeration<SearchResult> results = null;

        try {
            final Hashtable<String, String> ldapEnvironment = new Hashtable<>();
            ldapEnvironment.put(Context.INITIAL_CONTEXT_FACTORY, DEFAULT_JNDI_CONTEXT_FACTORY);
            ldapEnvironment.put(Context.PROVIDER_URL, "ldap://" + ldapHost + ":" + ldapPort + "/" + baseDomainName);
            ldapEnvironment.put(Context.SECURITY_PROTOCOL, "ssl");
            ldapEnvironment.put(Context.SECURITY_CREDENTIALS, password);
            ldapEnvironment.put(Context.SECURITY_PRINCIPAL, domainPrefix + '\\' + username);

            ldapEnvironment.put("com.sun.jndi.ldap.read.timeout", timeout);
            ldapEnvironment.put("com.sun.jndi.ldap.connect.timeout", timeout);

            ctx = new InitialLdapContext(ldapEnvironment, null);

            final SearchControls constraints = new SearchControls();
            constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
            constraints.setReturningAttributes(new String[]{"memberOf"});
            constraints.setReturningObjFlag(true);
            results = ctx.search("", "(sAMAccountName=" + username + ")", constraints);

            if (!results.hasMore()) {
                LOG.warn(".authenticate(" + ldapHost + "," + username + "): unable to locate " + username);
                return null;
            }

            final Set<String> adGroups = new TreeSet<>();
            final SearchResult entry = results.next();
            for (NamingEnumeration valEnum = entry.getAttributes().get("memberOf").getAll(); valEnum.hasMoreElements(); ) {
                String dn = (String) valEnum.nextElement();
                int i = dn.indexOf(",");
                if (i != -1) {
                    dn = dn.substring(0, i);
                }

                if (dn.startsWith("CN=")) {
                    dn = dn.substring("CN=".length());
                }
                adGroups.add(dn);
            }
            return adGroups;
        }
        finally {
            try {
                if (null != results)
                    results.close();
            }
            catch (Throwable ignored) {
            }
            try {
                if (null != ctx)
                    ctx.close();
            }
            catch (Throwable ignored) {
            }
        }
    }


    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(
                UsernamePasswordAuthenticationToken.class);
    }
}

Save yourself time by not rewriting your own LdapAuthenticationProvider, there is an existing ActiveDirectoryLdapAuthenticationProvider that will use the received credentials for authentication to LDAP and you can also add a searchFilter if you want to do more (e.g. see if user also belongs to a specific group)

Related docs:

https://docs.spring.io/spring-security/site/docs/3.1.x/reference/springsecurity-single.html#ldap-active-directory

https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProvider.html

Sample snippet:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private LdapProperties ldapProperties;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) {
        auth.authenticationProvider(activeDirectoryLdapAuthenticationProvider());
    }

    @Bean
    public AuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
        ActiveDirectoryLdapAuthenticationProvider authenticationProvider =
                new ActiveDirectoryLdapAuthenticationProvider(ldapProperties.getDomain(), ldapProperties.getProviderUrl());

        authenticationProvider.setConvertSubErrorCodesToExceptions(true);
        authenticationProvider.setUseAuthenticationRequestCredentials(true);
        //if you're not happy on the default searchFilter, you can set your own. See https://docs.spring.io/spring-security/site/docs/4.2.18.RELEASE/apidocs/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProvider.html#setSearchFilter-java.lang.String-
        authenticationProvider.setSearchFilter("(&(objectClass=user)(cn={1}))");
        return authenticationProvider;
    }
...
}
Related