Spring Security Active Directory LDAP Example

by | Jan 26, 2016

At a recent client, I was tasked with securing their web applications using Spring Security and their internal Active Directory (AD) LDAP server.

I scoured the web and went through a lot of trial and error until I finally found the configuration that worked in their environment.  Based on some of the comments and questions I found on the web, the problems that I was facing seemed to be shared by others. Here’s a Spring Security Active Directory example to show how I was finally able to get Spring Security to work with the Active Directory LDAP server.

Note: This article does not go into the details of using Spring Security.  You can find the Spring Security documentation at: Spring Security 4.0.3 Documentation.

Versions

The examples below are using:

  • Spring Security: 4.0.3.RELEASE
  • Spring (web, core, etc): 4.2.3.RELEASE

Configuration

Dependencies

Add the Spring Security dependencies to your application’s pom.xml file:

<!-- Spring Security -->
<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-web</artifactId>
	<version>4.0.3.RELEASE</version>
</dependency>
<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-config</artifactId>
	<version>4.0.3.RELEASE</version>
</dependency>
<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-ldap</artifactId>
	<version>4.0.3.RELEASE</version>
</dependency>

Deployment Descriptor

Add the Spring Security filter to your application’s web.xml file:

<filter>
	<filter-name>springSecurityFilterChain</filter-name>
	<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
	<filter-name>springSecurityFilterChain</filter-name>
	<url-pattern>/*</url-pattern>
</filter-mapping>

Also, in the web.xml, ensure that you are loading the context using the ContextLoaderListener  and the context is not loaded again by the DispatcherServlet :

<context-param>
	<param-name>contextConfigLocation</param-name>
	<!-- replace with your spring context file -->
	<param-value>/WEB-INF/mvc-dispatcher-servlet.xml</param-value>
</context-param>
<listener>
	<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<servlet>
	<servlet-name>mvc-dispatcher</servlet-name>
	<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
	<init-param>
		<!-- set to blank to ensure context is only loaded once -->
		<param-name>contextConfigLocation</param-name>
		<param-value></param-value>
	</init-param>
	<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
	<servlet-name>mvc-dispatcher</servlet-name>
	<!-- url base for your spring app, not security config.
		That is handled by the filter -->
	<url-pattern>/*</url-pattern>
</servlet-mapping>

Note: I had an issue where the Spring context would try to load and then unload with a failure after I added the <filter> to the web.xml.  To fix this, I had to move the Spring context file to the <context-param> and use the ContextLoaderListener as shown above.

Spring Context

Configure your Spring context:

<beans xmlns="http://www.springframework.org/schema/beans"
	...
	xmlns:sec="http://www.springframework.org/schema/security"
	xsi:schemaLocation="
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		...
		http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd">

	...
	
	<!-- Begin Web Security -->
	<!-- unsecured resource if needed -->
	<sec:http pattern="/images/**" security="none"/> 
	<sec:http>
	    <!-- configure the roles allowed to access the app -->
	    <sec:intercept-url pattern="/**" access="hasAnyRole('MANAGER', 'USER')"/>
	    <!-- add more urls/patters/roles to refine security -->
	    
	    <sec:form-login/>
	    <sec:logout/>
	    <!-- if you are adding to an exiting app, 
				you may need to disable CSRF protection until you can make application changes. -->
	    <!-- sec:csrf disabled="true"/ -->
	</sec:http>

	<!-- add the properties below to your app's properties file 
			or replace with hardcoded values to get working -->
	<sec:ldap-server 
		id="contextSource" 
		url="ldap://${ldap.server}:${ldap.port}/"
		manager-dn="${ldap.manager.user}"
		manager-password="${ldap.manager.password}"/>
		 
	<sec:authentication-manager erase-credentials="true">
		<sec:authentication-provider ref='ldapAuthProvider' />
	</sec:authentication-manager>
	 
	 
	<!-- using bean-based configuration here to set the DefaultLdapAuthoritiesPopulater with 
		ignorePartialResultsException=true.  This is a known Spring/AD issue and a workaround for it -->
	<bean id="ldapAuthProvider" class="org.springframework.security.ldap.authentication.LdapAuthenticationProvider">
		<constructor-arg>
			<!-- the bind authenticator will first lookup the user using the service account credentials, then 
				 attempt to bind the user with their password once found -->
			<bean id="bindAuthenticator" class="org.springframework.security.ldap.authentication.BindAuthenticator">
				<constructor-arg ref="contextSource" />
				<property name="userSearch" ref="userSearch" />
			</bean>
		</constructor-arg>
		<constructor-arg>
			<bean class="org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator">
				<constructor-arg ref="contextSource" />
				<constructor-arg value="DC=company,DC=com" /> <!-- group search base -->
				<!-- <property name="defaultRole" value="ROLE_USER" /> 
					You can add a default role to everyone if needed -->
				<property name="searchSubtree" value="true" />
				<property name="ignorePartialResultException" value="true" />
				<property name="groupSearchFilter" value="(member={0})" />
			</bean>
		</constructor-arg>
	</bean>
	<bean id="userSearch" class="org.springframework.security.ldap.search.FilterBasedLdapUserSearch">
		<constructor-arg index="0" value="DC=company,DC=com" />
		<constructor-arg index="1" value="(sAMAccountName={0})" />
		<constructor-arg index="2" ref="contextSource" />
		<property name="searchSubtree" value="true" />
	</bean>
	<!--  end Web Security -->
</beans>

Here are the properties used above:

# Ldap server access
ldap.server=LDAP01
ldap.port=389
ldap.manager.user=cn=LdapAuthUser,ou=ServiceAccounts,dc=company,dc=com
ldap.manager.password=SomePassword!

How it works

When the LdapAuthenticationProvider  is performing the authentication, it will:

  1. Bind to LDAP using the manager user id and password specified in the <sec:ldap-server>
  2. Perform a lookup on the user id (entered from the login screen) using the userSearch bean
  3. Get the fully distinguished name of the user that matches
  4. Use that user and the password (entered) to bind to LDAP again
  5. Search for all of the groups the user is in based on the groupSearchFilter configuration

If you set your root logger to “debug”, log entries detailing this process will be output (including search result details).  This was a huge help to me as I worked through the configuration.

To verify that there aren’t any LDAP configuration issues, or wrong credentials, you can use an LDAP browser (links below) to manually emulate this process.

 

Other Notes

  • For help with LDAP, it is nice to have an LDAP browser installed such as http://jxplorer.org/ or http://www.ldapadmin.org/
  • The configuration above will automatically make the /logout url available.  You should add a link to it in your application.
  • To get the username from inside a controller or service, use: SecurityContextHolder.getContext().getAuthentication().getName()