Spring 3.0 – Mapping Active Directory LDAP custom attributes to UserDetails

In earlier versions of Spring Security/Acegi, the UserDetails object contained a method called getAttributes(). This method would return the custom attributes assigned to a user. In the case of Active Directory, this contained all the little minutia about a user that a domain defines, a good example in my case was that it contained personal information about a user’s work location that I needed to access.

The getAttributes method was deprecated and removed in Spring 3.0, leaving no way to access these attributes. The documentation states that now in order to get at these attributes you need to define your own implementation of a UserDetailsMapper and populate your own implementation of a UserDetails object. I was really shocked that this functionality was completely removed, but I two good reasons I can think of is that the data is stored differently in other LDAP implementations and the attribute list could get quite large and since it gets carried around in memory and if you have several hundred users logging in…well you can see the memory problem.

I wrote my own implementation for this in hopes that it might help others. There isn’t any other implementation out there I could find so I was forced to write it from scratch.

First thing, we need to define this in our spring security setup.

<bean id="ldapAuthProvider"
class=”org.springframework.security.ldap.authentication.LdapAuthenticationProvider”>

<bean id="userDetailsContextMapper"
class=”com.domain.project.security.AttributesLDAPUserDetailsContextMapper”>

description

ROLE_Teller-Users
ROLE_iArchiveViewer_Backoffice
ROLE_Navigator Support

Note the sections in bold are the key sections you may or may not want to change. let’s look at our mapping class now, AttributesLDAPUserDetailsContextMapper.

import java.util.List;

import javax.naming.NamingEnumeration;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ldap.core.DirContextAdapter;

import org.springframework.ldap.core.DirContextOperations;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.ldap.userdetails.LdapUserDetailsImpl;
import org.springframework.security.ldap.userdetails.LdapUserDetailsMapper;
import org.springframework.util.Assert;

public class AttributesLDAPUserDetailsContextMapper extends
LdapUserDetailsMapper {

private Logger logger = LoggerFactory
.getLogger(AttributesLDAPUserDetailsContextMapper.class);

private String[] attributesToPopulate = new String[] {};
private String[] roleAttributes = null;
private boolean mapAllAttributes = false;

public boolean isMapAllAttributes() {
return mapAllAttributes;
}

public void setMapAllAttributes(boolean mapAllAttributes) {
this.mapAllAttributes = mapAllAttributes;
}

public String[] getAttributesToPopulate() {
return attributesToPopulate;
}

public void setAttributesToPopulate(String[] attributesToPopulate) {
this.attributesToPopulate = attributesToPopulate;
}

@Override
public UserDetails mapUserFromContext(DirContextOperations ctx,
String username, List authorities) {

// LdapUserDetailsImpl origImpl =
// (LdapUserDetailsImpl)super.mapUserFromContext(ctx, username,
// authorities);

String dn = ctx.getNameInNamespace();

logger.debug("Mapping user details from context with DN: " + dn);

ExtUserDetails.Essence essence = new ExtUserDetails.Essence();
essence.setDn(dn);

// Object passwordValue =
// ctx.getObjectAttribute(super.passwordAttributeName);

// if (passwordValue != null) {
// essence.setPassword(mapPassword(passwordValue));
// }

essence.setUsername(username);

// Map the roles
for (int i = 0; (roleAttributes != null) && (i < roleAttributes.length); i++) { String[] rolesForAttribute = ctx .getStringAttributes(roleAttributes[i]); if (rolesForAttribute == null) { logger.debug("Couldn't read role attribute '" + roleAttributes[i] + "' for user " + dn); continue; } for (int j = 0; j < rolesForAttribute.length; j++) { GrantedAuthority authority = createAuthority(rolesForAttribute[j]); if (authority != null) { essence.addAuthority(authority); } } } // Add the supplied authorities for (GrantedAuthority grantedAuthority : authorities) { essence.addAuthority(grantedAuthority); } DirContextAdapter dca = (DirContextAdapter) ctx;

for (int i = 0; i < attributesToPopulate.length; i++) { String attribute = dca.getStringAttribute(attributesToPopulate[i]); essence.addAttribute(attributesToPopulate[i], attribute); logger.debug("Custom Attribute: '" + attributesToPopulate[i] + "' =" + attribute); } if (isMapAllAttributes()) {

Attributes attributes = dca.getAttributes();

NamingEnumeration attributelist = attributes.getIDs();

while (attributelist.hasMoreElements()) {
String key = (String) attributelist.nextElement();

essence.addAttribute(key, attributes.get(key));
logger.debug("Attribute List: '" + key + "' ="
+ attributes.get(key));
}

}

return essence.createUserDetails();

}

@Override
public void setRoleAttributes(String[] roleAttributes) {
Assert.notNull(roleAttributes, "roleAttributes array cannot be null");
this.roleAttributes = roleAttributes;
}

}

There is some overlap here. I set mapAllAttributes to true in the xml, so it will place all of the LDAP attributes in the HashMap inside of UserDetails and I also could have set this to false and just mapped certains ones that I want to load instead of loading the entire list.

Now we need our new UserDetailsClass. This class is a carbon copy of LdapUserDetailsImpl with a few changes.


import java.util.HashMap;
import java.util.Map;
import java.util.ArrayList;
import java.util.List;

import javax.naming.Name;

import org.springframework.ldap.core.DirContextOperations;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.ldap.ppolicy.PasswordPolicyData;
import org.springframework.util.Assert;

import org.springframework.security.ldap.userdetails.LdapUserDetailsImpl;

public class ExtUserDetails extends LdapUserDetailsImpl {

//~ Instance fields ================================================================================================

private String dn;
private String password;
private String username;
private List authorities = AuthorityUtils.NO_AUTHORITIES;
private Map attributes = new HashMap();

private boolean accountNonExpired = true;
private boolean accountNonLocked = true;
private boolean credentialsNonExpired = true;
private boolean enabled = true;
// PPolicy data
private int timeBeforeExpiration = Integer.MAX_VALUE;
private int graceLoginsRemaining = Integer.MAX_VALUE;

//~ Constructors ===================================================================================================

protected ExtUserDetails() {}

//~ Methods ========================================================================================================
public Map getAttributes() {
return attributes;
}

public Object getAttribute(String key){

return attributes.get(key);
}

public List getAuthorities() {
return authorities;
}

public String getDn() {
return dn;
}

public String getPassword() {
return password;
}

public String getUsername() {
return username;
}

public boolean isAccountNonExpired() {
return accountNonExpired;
}

public boolean isAccountNonLocked() {
return accountNonLocked;
}

public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
}

public boolean isEnabled() {
return enabled;
}

public int getTimeBeforeExpiration() {
return timeBeforeExpiration;
}

public int getGraceLoginsRemaining() {
return graceLoginsRemaining;
}

public String toString() {
StringBuffer sb = new StringBuffer();
sb.append(super.toString()).append(": ");
sb.append("Username: ").append(this.username).append("; ");
sb.append("Password: [PROTECTED]; ");
sb.append("Enabled: ").append(this.enabled).append("; ");
sb.append("AccountNonExpired: ").append(this.accountNonExpired).append("; ");
sb.append("credentialsNonExpired: ").append(this.credentialsNonExpired).append("; ");
sb.append("AccountNonLocked: ").append(this.accountNonLocked).append("; ");

if (this.getAuthorities() != null) {
sb.append("Granted Authorities: ");

for (int i = 0; i 0) {
sb.append(", ");
}

sb.append(this.getAuthorities().get(i).toString());
}
} else {
sb.append("Not granted any authorities");
}

return sb.toString();
}

//~ Inner Classes ==================================================================================================

/**
* Variation of essence pattern. Used to create mutable intermediate object
*/
public static class Essence {
protected ExtUserDetails instance = createTarget();
private List mutableAuthorities = new ArrayList();
private Map mutableAttributes = new HashMap();

public Essence() { }

public Essence(DirContextOperations ctx) {
setDn(ctx.getDn());
}

public Essence(ExtUserDetails copyMe) {
setDn(copyMe.getDn());
setUsername(copyMe.getUsername());
setPassword(copyMe.getPassword());
setEnabled(copyMe.isEnabled());
setAccountNonExpired(copyMe.isAccountNonExpired());
setCredentialsNonExpired(copyMe.isCredentialsNonExpired());
setAccountNonLocked(copyMe.isAccountNonLocked());
setAuthorities(copyMe.getAuthorities());
setAttributes(copyMe.getAttributes());
}

protected ExtUserDetails createTarget() {
return new ExtUserDetails();
}

/** Adds the authority to the list, unless it is already there, in which case it is ignored */
public void addAttribute(String key, Object value) {
mutableAttributes.put(key, value);
}

/** Adds the authority to the list, unless it is already there, in which case it is ignored */
public void addAuthority(GrantedAuthority a) {
if(!hasAuthority(a)) {
mutableAuthorities.add(a);
}
}

private boolean hasAuthority(GrantedAuthority a) {
for (GrantedAuthority authority : mutableAuthorities) {
if(authority.equals(a)) {
return true;
}
}
return false;
}

public ExtUserDetails createUserDetails() {
Assert.notNull(instance, "Essence can only be used to create a single instance");
Assert.notNull(instance.username, "username must not be null");
Assert.notNull(instance.getDn(), "Distinguished name must not be null");

instance.authorities = getGrantedAuthorities();

ExtUserDetails newInstance = instance;

instance = null;

return newInstance;
}

public List getGrantedAuthorities() {
return mutableAuthorities;
}

public void setAccountNonExpired(boolean accountNonExpired) {
instance.accountNonExpired = accountNonExpired;
}

public void setAccountNonLocked(boolean accountNonLocked) {
instance.accountNonLocked = accountNonLocked;
}

public void setAuthorities(List authorities) {
mutableAuthorities = authorities;
}

public void setAttributes(Map attributes) {
mutableAttributes = attributes;
}

public void setCredentialsNonExpired(boolean credentialsNonExpired) {
instance.credentialsNonExpired = credentialsNonExpired;
}

public void setDn(String dn) {
instance.dn = dn;
}

public void setDn(Name dn) {
instance.dn = dn.toString();
}

public void setEnabled(boolean enabled) {
instance.enabled = enabled;
}

public void setPassword(String password) {
instance.password = password;
}

public void setUsername(String username) {
instance.username = username;
}

public void setTimeBeforeExpiration(int timeBeforeExpiration) {
instance.timeBeforeExpiration = timeBeforeExpiration;
}

public void setGraceLoginsRemaining(int graceLoginsRemaining) {
instance.graceLoginsRemaining = graceLoginsRemaining;
}
}
}

This class contains our hashmap where our custom attributes are stored.

We have all of our pieces, now we just need to access the attributes.

Object o = SecurityContextHolder.getContext().getAuthentication().getDetails();

if (o instanceof ExtUserDetails){
return (String) ((ExtUserDetails) o).getAttributes().get(“description”);
} else {
return o.toString();
}

We just cast the UserDetails object back to our instance so we can access the values it is holding for us.

For my taste, this is too many steps to providing a UserDetails mapping strategy. spring needs a better mechanism or it at least needs to include my methods above as an option for folks to use. A default strategy is better than none at all.

About these ads

About Chris Hardin
Chief Architect at Doozer Software in Birmingham, Al. I specialize in Java and .NET Architecture, Ajax Frameworks and Mobile Architecture with iOS and Android.

3 Responses to Spring 3.0 – Mapping Active Directory LDAP custom attributes to UserDetails

  1. Anonymous says:

    thank you, i was looking for this, shortcut :
    public class LdapUserDetailsInfo extends LdapUserDetailsImpl{

    private static final long serialVersionUID = 1L;

    private Map attributes = new HashMap();

    LdapUserDetailsInfo(UserDetails details) {
    copy(details,this);
    }

    private void copy(final UserDetails source,final UserDetails target){
    ReflectionUtils.doWithFields(source.getClass(), new FieldCallback(){
    public void doWith(Field field) throws IllegalArgumentException,
    IllegalAccessException {
    ReflectionUtils.makeAccessible(field);
    ReflectionUtils.setField(field, target, ReflectionUtils.getField(field, source));
    }},ReflectionUtils.COPYABLE_FIELDS);
    }

    void add(String key,Object value){
    attributes.put(key, value);
    }

    public Map getAttributes() {
    return attributes;
    }

  2. Anonymous says:

    hi,
    We Need a clue,
    After implementing your solution, getAttributes().size() returns 0, I tryed with principal() instead of userDetails but it returns no attributes
    What's wrong ??
    Thanks in advance!

  3. gregor says:

    i think you missed out a line:

    instance.attributes = mutableAttributes;

    in:

    public ExtUserDetails createUserDetails() {
    }

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: