Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to use Spring Security (3.1.X) to get LDAP information on a user other that the one authenticated against?

I use Spring Security to authenticate a user against an Active Directory server. A CustomUserContext is also injected into the ldapAuthenticationProvider bean to provide access to additional LDAP attributes. Everything works quite well. I have no problem pulling whatever I want from the authenticated user.

The issue I have is that I want to retrieve some attributes, most specifically the email address, from the Active Directory server on a user other than the user that is logged in. Is it possible to achieve this by leveraging what I already have, or is my only option to use a totally separate method to access LDAP attributes from a different user?

[edit] Configuration follows

security-config.xml

<?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:sec="http://www.springframework.org/schema/security"
       xmlns:security="http://www.springframework.org/schema/security"
       xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd
       http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd">

<bean id="contextSource" class="org.springframework.ldap.core.support.LdapContextSource">
    <property name="url" value="ldap://xxxx.xxxx.xxx:389" />
    <property name="base" value="dc=corp,dc=global,dc=xxxxx,dc=com" />
    <property name="userDn" value="CN=lna.authquery,OU=LDAPGroups,OU=NorthAmerica,DC=corp,DC=global,DC=xxxxx,DC=com" />
    <property name="password" value="xxxxxxx" />
    <property name="pooled" value="true" />
    <!-- AD Specific Setting for avoiding the partial exception error -->
    <property name="referral" value="follow" />
</bean>

<bean id="ldapAuthenticationProvider" class="org.springframework.security.ldap.authentication.LdapAuthenticationProvider" >
    <constructor-arg>
        <bean class="org.springframework.security.ldap.authentication.BindAuthenticator">
            <constructor-arg ref="contextSource" />
            <property name="userSearch">
                <bean id="userSearch" class="org.springframework.security.ldap.search.FilterBasedLdapUserSearch">
                    <constructor-arg index="0" value="" />
                    <constructor-arg index="1" value="(sAMAccountName={0})" />
                    <constructor-arg index="2" ref="contextSource" />
                </bean>
            </property>
        </bean>
    </constructor-arg>
    <constructor-arg>
        <bean class="org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator">
            <constructor-arg ref="contextSource" />
            <constructor-arg value="" />
            <property name="groupSearchFilter" value="(member={0})" />
            <property name="searchSubtree" value="true" />
            <!-- Settings below convert the adds the prefix ROLE_ to roles returned from AD -->
        </bean>
    </constructor-arg>
    <property name="userDetailsContextMapper">
       <bean class="net.xxxx.xxxxx.utilities.CustomUserDetailsContextMapper" />
    </property>
</bean>

<bean id="authenticationManager" class="org.springframework.security.authentication.ProviderManager">
    <constructor-arg>
        <list>
            <ref local="ldapAuthenticationProvider" />
        </list>
    </constructor-arg>
</bean>

<sec:http pattern="/css/**" security="none"/>
<sec:http pattern="/images/**" security="none"/>
<sec:http auto-config="true" authentication-manager-ref="authenticationManager" >
    <sec:intercept-url pattern="/login.jsp*" requires-channel="https" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
    <sec:intercept-url pattern="/**" requires-channel="https" access="IS_AUTHENTICATED_FULLY"/>
    <sec:form-login login-page='/login.jsp' 
                    default-target-url="/home.html" 
                    authentication-failure-url="/login.jsp" />
</sec:http>  

CustomeUserDetails.java

package net.xxxx.xxxx.utilities;

import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;

public class CustomUserDetails extends User {

    private static final long serialVersionUID = 1416132138315457558L;

     // extra instance variables
       final String fullname;
       final String email;
       final String title;

       public CustomUserDetails(String username, String password, boolean enabled, boolean accountNonExpired,
             boolean credentialsNonExpired, boolean accountNonLocked,
             Collection<? extends GrantedAuthority> authorities, String fullname,
             String email, String title) {

           super(username, password, enabled, accountNonExpired, credentialsNonExpired,
                accountNonLocked, authorities);

           this.fullname = fullname;
           this.email = email;
           this.title = title;
       }

       public String getFullname() {
           return this.fullname;
       }

       public String getEmail() {
           return this.email;
       }

       public String getTitle() {
           return this.title;
       }
}

CustomUserDetailsContextMapper.java

package net.xxxx.xxxxx.utilities;

import java.util.Collection;

public class CustomUserDetailsContextMapper implements UserDetailsContextMapper {

    public UserDetails mapUserFromContext(DirContextOperations ctx,
            String username, Collection<? extends GrantedAuthority> authorities) {

        String fullname = "";
        String email = "";
        String title = "";

        Attributes attributes = ctx.getAttributes();
        try {
            fullname = (String) attributes.get("displayName").get(); 
            email = (String) attributes.get("mail").get(); 
            title = (String) attributes.get("title").get(); 
        } catch (NamingException e) {
            e.printStackTrace();
        }

        CustomUserDetails details = new CustomUserDetails(username, "", true, true, true, true, authorities, fullname, email, title);
        return details;
    }

    public void mapUserToContext(UserDetails user, DirContextAdapter ctx) {

    }

}
like image 527
Bill Avatar asked Oct 05 '12 15:10

Bill


People also ask

How do I authenticate someone using LDAP?

In order to authenticate a user with an LDAP directory you first need to obtain their DN as well as their password. With a login form, people typically enter a simple identifier such as their username or email address. You don't expect them to memorise the DN of their directory entry.

How does spring boot LDAP work?

Spring Boot provides auto-configuration for an embedded server written in pure Java, which is being used for this guide. The ldapAuthentication() method configures things so that the user name at the login form is plugged into {0} such that it searches uid={0},ou=people,dc=springframework,dc=org in the LDAP server.


2 Answers

I finally did end up figuring out how to do this. I'm answering this in case it helps someone else who needs to do this. I'd be surprised if I'm the only one.

First I had to move my security-config.xml file out of the WEB-INF structure and put it under the spring resources directory. The contextSource bean I was able to reuse. However I could not reuse the CustomUserDetailsContextMapper.java nor the CustomUserDetails.java class as they were too specific to Spring security and not to just retrieving LDAP data from an unauthenticated user.

I ended up writing a separate class for the LDAP access that had the common contextSource autowired in. That class is below.

LdapDao.java

package net.xxxxx.xxx.dao;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;

import javax.naming.directory.Attributes;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ldap.core.AttributesMapper;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.core.support.LdapContextSource;
import org.springframework.stereotype.Component;

@Component
public class LdapDao {

    LdapTemplate template;

    @Autowired
    public LdapDao(LdapContextSource contextSource) {
        template = new LdapTemplate(contextSource);
    }

    @SuppressWarnings("unchecked")
    public Map<String, String> getUserAttributes(String username) {
        Map<String, String> results = new HashMap<String, String>();

        String objectClass = "samAccountName=" + username;
        LinkedList<Map<String, String>> list = (LinkedList<Map<String, String>>) template.search("", objectClass, new UserAttributesMapper());
        if (!list.isEmpty()) {
            // Should only return one item
            results = list.get(0);
        }
        return results;
    }

    private class UserAttributesMapper implements AttributesMapper {

        @Override
        public Map<String, String> mapFromAttributes(Attributes attributes) throws javax.naming.NamingException {
            Map<String, String> map = new HashMap<String, String>();

            String fullname = (String) attributes.get("displayName").get(); 
            String email = (String) attributes.get("mail").get(); 
            String title = (String) attributes.get("title").get();

            map.put("fullname", fullname);
            map.put("email", email);
            map.put("title", title);
            return map;
        }
    }   
}
like image 59
Bill Avatar answered Nov 08 '22 10:11

Bill


@Bill what you've done is great, though there is actually an easier way. Instead of resorting to the LdapTemplate, just use the beans you've already registered for DefaultLdapAuthoritiesPopulator and FilterBasedLdapUserSearch. This way you can get the same UserDetails object which also has the authorities populated and reuses your existing code for your net.xxxx.xxxxx.utilities.CustomUserDetailsContextMapper.

Here's what you need to do:

  1. Split out the beens you need to inject as named beans and use ref attributes for the properties and constructor-args (DefaultLdapAuthoritiesPopulator, FilterBasedLdapUserSearch, net.xxxx.xxxxx.utilities.CustomUserDetailsContextMapper).
  2. In your LdapDao inject references to:
    • FilterBasedLdapUserSearch - userSearch
    • DefaultLdapAuthoritiesPopulator - authPop
    • net.xxxx.xxxxx.utilities.CustomUserDetailsContextMapper - userMapper
  3. Add the following method to your LdapDao:

.

public UserDetails getUserDetails(final String username) {
    try {
        DirContextOperations ctx = userSearch.searchForUser(username);
        return userMapper.mapUserFromContext(ctx, username,
                authPop.getGrantedAuthorities(ctx, username));
    } catch (UsernameNotFoundException ex) {
        return null;
    }
}

Now you can just call getUserDetails(String) to get the same object you do when retrieving the currently logged in context, and can use the same code etc.

like image 23
Brett Ryan Avatar answered Nov 08 '22 09:11

Brett Ryan