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) {
}
}
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.
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.
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;
}
}
}
@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:
ref
attributes for the properties and constructor-arg
s (DefaultLdapAuthoritiesPopulator
, FilterBasedLdapUserSearch
, net.xxxx.xxxxx.utilities.CustomUserDetailsContextMapper
).LdapDao
inject references to:
FilterBasedLdapUserSearch
- userSearch
DefaultLdapAuthoritiesPopulator
- authPop
net.xxxx.xxxxx.utilities.CustomUserDetailsContextMapper
- userMapper
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With