Hi,
one thing that can be very trick is how to configure a Spring Security with LDAP using Active Directory as the underlying authentication system.
Spring Security documentation lacks on information about Active Directory. The tick thing is that we need to extend Spring Security LDAP authentication components to successfully implement it in our JSF or Dynamic web application.
In this tutorial, I'm using:
Maven2 with Simple Web Application skipping the Archtypes ( I don't like them since we always after need to clean up the code)
Spring 3
Spring Security
So, let's start:
Step 1 - Configure your web application for using Spring and Spring Security
/src/main/webapp/WEB-INF/web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<display-name>WebApplication</display-name>
<session-config>
<session-timeout>10</session-timeout>
</session-config>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:WebApplication-context.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<listener>
<listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
</listener>
<listener>
<listener-class>
org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
</listener>
<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>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>
2 - Configure the Spring context
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:jee="http://www.springframework.org/schema/jee"
xmlns:cxf="http://camel.apache.org/schema/cxfEndpoint"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:security="http://www.springframework.org/schema/security"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/jee
http://www.springframework.org/schema/jee/spring-jee-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.0.3.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util-3.0.xsd
">
<context:annotation-config/>
<context:component-scan base-package="com.yourcompany.services" />
<tx:annotation-driven />
<security:http auto-config='false' >
<security:intercept-url pattern="/login.jsp*" access="IS_AUTHENTICATED_ANONYMOUSLY" />
<security:intercept-url pattern="/app/*" access="ROLE_USER" />
<security:form-login login-page='/login.jsp' authentication-failure-url="/login.jsp?authfailed=true" />
<security:session-management session-fixation-protection="none" > </security:session-management>
<security:anonymous />
<security:http-basic />
<security:logout />
</security:http>
<security:ldap-server id="ldapServer" url="ldap://your_active_directory:389/" />
<security:authentication-manager>
<security:authentication-provider ref="ldapAuthenticationProvider" />
</security:authentication-manager>
<bean id="contextSource"
class="org.springframework.security.ldap.DefaultSpringSecurityContextSource">
<constructor-arg value="ldap://your_active_directory_ip:389/" />
</bean>
<bean id="ldapAuthenticationProvider"
class="com.yourcompany.services.security.LdapAuthenticationProvider">
<property name="authenticator" ref="ldapAuthenticator" />
</bean>
<bean id="ldapAuthenticator"
class="com.yourcompany.services.security.LdapAuthenticatorImpl">
<property name="contextFactory" ref="contextSource" />
<property name="principalPrefix" value="" />
</bean>
</beans>
3 - Here is the trick, you must create LdapAuthenticationProvider, LdapAuthenticatorImpl, LdapAuthenticationToken and Principal
Here are the source codes:
package com.yourcompany.services.security;
import javax.naming.ldap.InitialLdapContext;
import org.springframework.ldap.AuthenticationException;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.ldap.authentication.LdapAuthenticator;
public class LdapAuthenticationProvider implements AuthenticationProvider {
private LdapAuthenticator authenticator;
public Authentication authenticate(Authentication auth) throws AuthenticationException {
// Authenticate, using the passed-in credentials.
DirContextOperations authAdapter = authenticator.authenticate(auth);
// Creating an LdapAuthenticationToken (rather than using the existing Authentication
// object) allows us to add the already-created LDAP context for our app to use later.
LdapAuthenticationToken ldapAuth = new LdapAuthenticationToken(auth, "ROLE_USER");
InitialLdapContext ldapContext = (InitialLdapContext) authAdapter.getObjectAttribute("ldapContext");
if (ldapContext != null) {
ldapAuth.setContext(ldapContext);
}
return ldapAuth;
}
public boolean supports(Class clazz) {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(clazz));
}
public LdapAuthenticator getAuthenticator() {
return authenticator;
}
public void setAuthenticator(LdapAuthenticator authenticator) {
this.authenticator = authenticator;
}
}
package com.yourcompany.services.security;
import javax.naming.ldap.InitialLdapContext;
import org.springframework.ldap.core.DirContextAdapter;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.security.core.Authentication;
import org.springframework.security.ldap.DefaultSpringSecurityContextSource;
import org.springframework.security.ldap.authentication.LdapAuthenticator;
public class LdapAuthenticatorImpl implements LdapAuthenticator {
private DefaultSpringSecurityContextSource contextFactory;
private String principalPrefix = "";
public DirContextOperations authenticate(Authentication authentication) {
// Grab the username and password out of the authentication object.
String principal = authentication.getName();
String password = "";
if (authentication.getCredentials() != null) {
password = authentication.getCredentials().toString();
}
// If we have a valid username and password, try to authenticate.
if (!("".equals(principal.trim())) && !("".equals(password.trim()))) {
try {
InitialLdapContext ldapContext = (InitialLdapContext) contextFactory.getContext(principal+"@YOUR_COMPANY_WINDOWS_DOMAIN",password);
DirContextOperations authAdapter = new DirContextAdapter();
authAdapter.addAttributeValue("ldapContext", ldapContext);
return authAdapter;
} catch(Exception e){
e.printStackTrace();
throw new org.springframework.security.authentication.BadCredentialsException("Login/Password not allowed to login.");
}
// We need to pass the context back out, so that the auth provider can add it to the
// Authentication object.
} else {
throw new org.springframework.security.authentication.BadCredentialsException("Login/Password not allowed to login.");
}
}
/**
* Since the InitialLdapContext that's stored as a property of an LdapAuthenticationToken is
* transient (because it isn't Serializable), we need some way to recreate the
* InitialLdapContext if it's null (e.g., if the LdapAuthenticationToken has been serialized
* and deserialized). This is that mechanism.
*
* @param authenticator
* the LdapAuthenticator instance from your application's context
* @param auth
* the LdapAuthenticationToken in which to recreate the InitialLdapContext
* @return
*/
static public InitialLdapContext recreateLdapContext(LdapAuthenticator authenticator,
LdapAuthenticationToken auth) {
DirContextOperations authAdapter = authenticator.authenticate(auth);
InitialLdapContext context = (InitialLdapContext) authAdapter
.getObjectAttribute("ldapContext");
auth.setContext(context);
return context;
}
public DefaultSpringSecurityContextSource getContextFactory() {
return contextFactory;
}
/**
* Set the context factory to use for generating a new LDAP context.
*
* @param contextFactory
*/
public void setContextFactory(DefaultSpringSecurityContextSource contextFactory) {
this.contextFactory = contextFactory;
}
public String getPrincipalPrefix() {
return principalPrefix;
}
/**
* Set the string to be prepended to all principal names prior to attempting authentication
* against the LDAP server. (For example, if the Active Directory wants the domain-name-plus
* backslash prepended, use this.)
*
* @param principalPrefix
*/
public void setPrincipalPrefix(String principalPrefix) {
if (principalPrefix != null) {
this.principalPrefix = principalPrefix;
} else {
this.principalPrefix = "";
}
}
}
package com.yourcompany.services.security;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.InitialLdapContext;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.GrantedAuthorityImpl;
public class LdapAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = -5040340622950665401L;
private Authentication auth;
transient private InitialLdapContext context;
private List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
/**
* Construct a new LdapAuthenticationToken, using an existing Authentication object and
* granting all users a default authority.
*
* @param auth
* @param defaultAuthority
*/
public LdapAuthenticationToken(Authentication auth, GrantedAuthority defaultAuthority) {
super(auth.getAuthorities());
this.auth = auth;
super.setAuthenticated(true);
}
/**
* Construct a new LdapAuthenticationToken, using an existing Authentication object and
* granting all users a default authority.
*
* @param auth
* @param defaultAuthority
*/
public LdapAuthenticationToken(Authentication auth, String defaultAuthority) {
this(auth, new GrantedAuthorityImpl(defaultAuthority));
}
public String nome;
public boolean alreadyLogged = false;
public Collection<GrantedAuthority> getAuthorities() {
if (!alreadyLogged) {
GrantedAuthority[] authoritiesArray = this.authorities.toArray(new GrantedAuthority[0]);
SearchControls searchCtls = new SearchControls();
String returnedAtts[] ={ "cn", "memberOf" };
searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);
searchCtls.setReturningAttributes(returnedAtts);
try {
NamingEnumeration answer = context.search("DC=yourcompany,DC=com", "(&(objectClass=*)(sAMAccountName=" + auth.getPrincipal() + "))", searchCtls);
System.out.println("USERS ROLES: " + answer);
while (answer.hasMoreElements())
{
SearchResult sr = (SearchResult) answer.next();
Attributes attrs = sr.getAttributes();
Map amap = null;
if (attrs != null)
{
amap = new HashMap();
NamingEnumeration ne = attrs.getAll();
while (ne.hasMore())
{
Attribute attr = (Attribute) ne.next();
if (attr.get()!= null) {
if (attr.getID().equals("cn")) {
nome = attr.get() + "";
} else if(attr.getID().equals("memberOf")) {
@SuppressWarnings("rawtypes")
NamingEnumeration enu = attr.getAll();
while(enu.hasMoreElements()) {
//StringTokenizer token = new StringTokenizer(attr.get() + "", ";");
String tk = new String (enu.nextElement() + "");
System.out.println(">>>>>>>>>>>>>>>>>>>>> TK: " + tk);
System.out.println("Already Logged: " + alreadyLogged);
if (tk.contains("HBWebAdmin")){
System.out.println("ROLE_USER");
GrantedAuthority admin = new GrantedAuthorityImpl("ROLE_USER");
authorities.add(admin);
}
if (tk.contains("HBWebAdmin") && tk.contains("ADMIN")){
System.out.println("ROLE_ADMIN");
GrantedAuthority admin = new GrantedAuthorityImpl("ROLE_ADMIN");
authorities.add(admin);
}
if (tk.contains("HBWebAdmin") && tk.contains("MNG_REPORT")){
System.out.println("Adding role: ROLE_MNG_REPORT");
GrantedAuthority user = new GrantedAuthorityImpl("ROLE_MNG_REPORT");
authorities.add(user);
}
if (tk.contains("HBWebAdmin") && tk.contains("MNG_REGISTER")){
System.out.println("Adding role: ROLE_MNG_REGISTER");
GrantedAuthority user = new GrantedAuthorityImpl("ROLE_MNG_REGISTER");
authorities.add(user);
}
if (tk.contains("HBWebAdmin") && tk.contains("MNG_LOGIN_MNG")){
System.out.println("Adding role: ROLE_MNG_LOGIN_MNG" );
GrantedAuthority user = new GrantedAuthorityImpl("ROLE_MNG_LOGIN_MNG");
authorities.add(user);
}
if (tk.contains("HBWebAdmin") && tk.contains("LEVEL1")){
System.out.println("Adding role: ROLE_LEVEL1" );
GrantedAuthority user = new GrantedAuthorityImpl("ROLE_LEVEL1");
authorities.add(user);
}
if (tk.contains("HBWebAdmin") && tk.contains("LEVEL2")){
System.out.println("Adding role: ROLE_LEVEL2" );
GrantedAuthority user = new GrantedAuthorityImpl("ROLE_LEVEL2");
authorities.add(user);
}
if (tk.contains("HBWebAdmin") && tk.contains("LEVEL3")){
System.out.println("Adding role: ROLE_LEVEL3" );
GrantedAuthority user = new GrantedAuthorityImpl("ROLE_LEVEL3");
authorities.add(user);
}
}
}
}
}
ne.close();
alreadyLogged = true;
}
}
} catch (NamingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return this.authorities;
}
public void addAuthority(GrantedAuthority authority) {
this.authorities.add(authority);
}
public Object getCredentials() {
return auth.getCredentials();
}
public Object getPrincipal() {
com.yourcompany.services.security.Principal p = new com.yourcompany.services.services.security.Principal(name);
return p;
}
/**
* Retrieve the LDAP context attached to this user's authentication object.
*
* @return the LDAP context
*/
public InitialLdapContext getContext() {
return context;
}
/**
* Attach an LDAP context to this user's authentication object.
*
* @param context
* the LDAP context
*/
public void setContext(InitialLdapContext context) {
this.context = context;
}
}
package com.yourcompany.services.security;
public class Principal implements java.security.Principal {
private String name;
public Principal(String name){
this.name = name;
}
@Override
public String getName() {
return name;
}
}
The trick thing is that you need to use your Context root of the Active Directory tree and pass the Active Directory Search like this
NamingEnumeration answer = context.search("DC=yourcompany,DC=com",
"(&(objectClass=*)(sAMAccountName=" + auth.getPrincipal() + "))",
searchCtls);
The above search, searches for the specific properties specified by searchCtls using the objectClass=* and sAMAccountName= userid, so this way your code is not tied to only one context in the Active Directory.
Pay attention to the bind authentication
InitialLdapContext ldapContext = (InitialLdapContext)
contextFactory.getContext(principal+"@YOUR_COMPANY_WINDOWS_DOMAIN",password);
you need to pass user_id@YOURCOMPANYDOMAIN and the password, so Active Directory will handle the password comparison when trying to get the context.