5
votes

We are using spring security to authenticate users from LDAP in our application. The authentication part is working properly but the authorization part is not working.

We are not able to retrieve the roles of the user from the LDAP.

From the book "Spring Security 3" by Peter Mularien

"This is because Active Directory stores group membership as attributes on the LDAP entries of users themselves. Out of the box (as of the time of publishing), Spring Security does not offer an LdapAuthoritiesPopulator that can be configured to support the structure of a typical Active Directory LDAP tree."

Below is my spring-security configuration file.

<?xml version="1.0" encoding="UTF-8"?>

<beans:beans xmlns="http://www.springframework.org/schema/security"
    xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:context="http://www.springframework.org/schema/context"
    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.xsd
                            http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.0.xsd">

       <http use-expressions="true" >
        <intercept-url pattern="/resources/**" filters="none" />
        <intercept-url pattern="/login" access="permitAll"/>
        <intercept-url pattern="/**" access="isAuthenticated()" />
        <form-login login-page="/login" 
                    default-target-url="/home" 
                    always-use-default-target="true"  
                    authentication-failure-url="/login?login_error=1" />
        <logout invalidate-session="true"
                logout-success-url="/"
                logout-url="/logout"/>
    </http>

    <authentication-manager alias="ldapAuthenticationManager">  
        <authentication-provider ref="ldapAuthenticationProvider"/>  
    </authentication-manager> 

    <beans:bean id="ldapAuthenticationProvider" class="org.springframework.security.ldap.authentication.LdapAuthenticationProvider">  
        <beans:constructor-arg ref="ldapBindAuthenticator"/>  
        <beans:constructor-arg ref="ldapAuthoritiesPopulator"/>  
        <beans:property name="userDetailsContextMapper" ref="ldapUserDetailsContextMapper"/>  
    </beans:bean> 

    <beans:bean id="ldapServer" class="org.springframework.security.ldap.DefaultSpringSecurityContextSource">  
        <!-- MS Active Directory -->  
        <beans:constructor-arg value="ldap://localhost:389/dc=myOrg,dc=net"/>  
        <beans:property name="userDn" value="admin"/>  
        <beans:property name="password" value="admin"/>
        <beans:property name="baseEnvironmentProperties">
            <beans:map>
                <beans:entry key="java.naming.referral" value="follow" />
            </beans:map>
        </beans:property>
    </beans:bean>  

    <beans:bean id="ldapBindAuthenticator" class="org.springframework.security.ldap.authentication.BindAuthenticator">  
        <beans:constructor-arg ref="ldapServer"/>  
        <beans:property name="userSearch" ref="ldapSearchBean"/>  
    </beans:bean>  

    <beans:bean id="ldapSearchBean" class="org.springframework.security.ldap.search.FilterBasedLdapUserSearch">  
        <!-- MS Active Directory -->  
        <!-- user-search-base; relative to base of configured context source -->  
        <beans:constructor-arg value="ou=Software OU"/>  
        <!-- user-search-filter -->  
        <beans:constructor-arg value="(sAMAccountName={0})"/>  
        <beans:constructor-arg ref="ldapServer"/>  
    </beans:bean>  

    <beans:bean id="ldapAuthoritiesPopulator" class="org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator">
        <beans:constructor-arg ref="ldapServer" />
        <beans:constructor-arg value="" />
        <beans:property name="groupSearchFilter" value="(sAMAccountName={0})"/>
        <beans:property name="groupRoleAttribute" value="memberOf" />
        <beans:property name="rolePrefix" value=""/>
        <beans:property name="searchSubtree" value="true"/>
        <beans:property name="convertToUpperCase" value="false"/>
        <beans:property name="ignorePartialResultException" value="true"/>
    </beans:bean>

    <beans:bean class="org.springframework.security.ldap.userdetails.InetOrgPersonContextMapper" id="ldapUserDetailsContextMapper"/> 

</beans:beans>

Please help.

1
What was missing in your code? Accepting the provided solution/hyperlink is one thing, but pointing out the missing part would be great for helping others (like me) having the exact same problem. Thank you for sharing your detailed solution.Charles Morin
@CharlesMorin I realized my answer was sub-par, sorry. Added our Spring config for AD.Marcel Stör
@MarcelStör Thank you. What application server are you using? I'm trying to have the same thing working on JBoss AS 7.2, without any success. Will take a look at your configuration.Charles Morin
@CharlesMorin, we use Tomcat. However, I don't understand why this should make any difference. The container shouldn't really be involved in the communication between Spring and the AD.Marcel Stör
@MarcelStör Yes it make a difference as JBoss is a Java EE container, which tends to preauthenticate and add some other layers of complexity in comparison to a standard servlet container like Tomcat. Thank you for your answer. I will try to make it work on JBoss and then provide the solution here once done.Charles Morin

1 Answers

2
votes

You might want to take a look here: https://jira.springsource.org/browse/SEC-876. Although this code contribution was declined, with a reasonable answer, it might give you hints.

We use the following config:

Spring XML

<bean id="ldapUserService" class="MyUserDetailService">
  <constructor-arg ref="ldapUserSearch"/>
  <constructor-arg ref="ldapAuthoritiesPopulator"/>
</bean>
<bean id="ldapUserSearch" class="org.springframework.security.ldap.search.FilterBasedLdapUserSearch">
  <constructor-arg value="OU=FOO-Accounts,OU=FOO,OU=OU-GLOBAL"/> <!-- user search base, RELATIVE TO SERVER CONTEXT (URL & base of configured LDAP server)! -->
  <constructor-arg value="(sAMAccountName={0})"/> <!-- user search filter -->
  <constructor-arg ref="ldapServer"/>
</bean>
<bean id="ldapAuthoritiesPopulator" class="MyLdapAuthoritiesPopulator">
  <constructor-arg ref="ldapServer" />
  <constructor-arg value="=OU=SomeFooBar,OU=FOO-Global-Security,OU=FOO-Groups,OU=FOO,OU=OU-GLOBAL" /> <!-- group search base, RELATIVE TO SERVER CONTEXT (URL & base of configured LDAP server)! -->
  <constructor-arg ref="roleMappings"/>
  <property name="groupRoleAttribute" value="cn" />
  <property name="groupSearchFilter" value="(member={0})" />
</bean>

Populator

There's a lot of proprietary code I cannot share because our customer has extra information in the AD we need to extract. I removed that as its of no concern for the question. Hence, this code won't compile.

public class MyLdapAuthoritiesPopulator extends DefaultLdapAuthoritiesPopulator {

  /**
   * Prefix assigned by Spring Security to each group/role from LDAP.
   */
  public static final String AUTHORITY_ROLE_PREFIX = "ROLE_";

  private Properties roleMappings;
  private Properties invertedRoleMappings;

  /**
   *
   * @param contextSource supplies the contexts used to search for user roles.
   * @param groupSearchBase if this is an empty string the search will be performed from the root DN
   * of the context factory. If null, no search will be performed.
   * @param roleMappings maps logical (internal) role names to names as delivered by LDAP
   */
  @SuppressWarnings("deprecation")
  public MyLdapAuthoritiesPopulator(final ContextSource contextSource,
      final String groupSearchBase,
      final Properties roleMappings) {
    super(contextSource, groupSearchBase);
    setConvertToUpperCase(false);
    setRolePrefix("");
    this.roleMappings = roleMappings;
    this.invertedRoleMappings = invertRoleMappings();
    logger.info("Processing LDAP roles based on the following mapping: {}.", roleMappings);
  }

  .....

  @Override
  public Set<GrantedAuthority> getGroupMembershipRoles(final String userDn, final String username) {
    final Set<GrantedAuthority> effectiveGroupMembershipRoles = super.getGroupMembershipRoles(
        userDn, username);
    return mapEffectiveRolesToApplicationRoles(effectiveGroupMembershipRoles);
  }

  /**
   * Maps effective LDAP roles such as 'foo_boston_dispatcher' or 'foo_boston_readonly' to
   * FOO internal roles. The internal role (i.e. the {@link GrantedAuthority}) is a combination
   * of the 'ROLE_' prefix and a {@link Role} enum value. .........
   */
  Set<GrantedAuthority> mapEffectiveRolesToApplicationRoles(final Set<GrantedAuthority> effectiveGroupMembershipRoles) {
    logger.info("Processing effective roles from LDAP: {}.", effectiveGroupMembershipRoles);
    final Set<GrantedAuthority> internalRoles = new HashSet<GrantedAuthority>();
    final List<String> effectiveRoleNames = extractRoleNamesFrom(effectiveGroupMembershipRoles);
    final List<String> unmappedGroupMembershipRoles = new ArrayList<String>();
    ......
    // in a method invoked here we do something like internalRoles.add(new GrantedAuthority(AUTHORITY_ROLE_PREFIX + role));
    ......
    logger.info("Created internal roles {}.", internalRoles);
    logger.trace(
        "The following group membership roles were not mapped to an internal equivalent: {}",
        unmappedGroupMembershipRoles);
    return internalRoles;
  }

  ......

  private List<String> extractRoleNamesFrom(final Collection<GrantedAuthority> authorities) {
    final List<String> authorityNames = new ArrayList<String>(authorities.size());
    for (GrantedAuthority authority : authorities) {
      authorityNames.add(authority.getAuthority());
    }
    return authorityNames;
  }
}