Introduction
This document describes how to get extended user data, such as an email address, from Active Directory (AD).
A previous article described how to do so using native tools, such as
COM4J. These tools, however, can be cumbersome to use, while using pure Java is much simpler and, hence, better: after all, what can be better and easier than a few simple calls to built-in Java packages?
Motivation
I had to deal with this issue while implementing an SSO (single sign on) project.
Implementing SSO over an AD is much more straight-forward in "native" languages such as C++ or even C#. In Java, however, things are more challenging and it took me a while to find this solution, which doesn’t need any native tools such as COM4J, or JNI calls to another C/C# program.
Projects such as Waffle make life much easier. It implements the negotiation between Windows, on the local machine, and the Active Directory, thus performing the SSO mechanism. However, even Waffle – which helps in many ways – has its limitations. For example, even though it does take care of the authentication, it cannot retrieve all the desired parameters from the AD. Using Waffle to retrieve the user's email address, telephone number, address, etc., is not possible.
To overcome this issue, one option is to use native tools, like COM4J. COM4J indeed works great, but its drawback is that it requires additional understanding and has its pitfalls. If everything works properly everyone is happy, but once problems arise, one has to dig in and resolve problems in corners no one ever really wanted to enter. For example, using COM4J forces the developer to include the relevant JARs in the build-path, or to worry about the COM4J.DLL version which is installed in the “web-inf/lib” directory (32/64? AMD?), etc.
This document shows how to use Java for this purpose, without the need of any other native tools, or any other dependencies. As an aside, I will mention that once everything is working, and you want to improve performance a bit, you can use Spring for LDAP, but let us leave this to the end.
The Code
I divided the code into three parts. The first part connects to the AD. The second uses the connection details, such as Context
and SearchBase
, and brings the data we want from the AD. The last part – well, this is the code that uses the first two parts to show how it works. I use Spring 3 to declare our beans as “Components
”, and to autowire these beans to the class that uses them. A full discussion of this is out of scope here, and I assume the reader knows how to use Spring.
ActiveDirectoryConnectionUtils
The ActiveDirectoryConnectionUtils
takes care of the connection. Explaining how to work with Java’s connection pool is out of scope of this document; for more about LDAP connection pooling, read Oracle’s “LDAP Connections” section.
@Component
public class ActiveDirectoryConnectionUtils
{
public LdapContext createContext(String url, String user, String pass)
{ Hashtable<String,String> env = getProperties(url, user, pass);
LdapContext ctx;
try
{
ctx = new InitialLdapContext(env, null);
}
catch (NamingException e)
{
throw new RuntimeException(e);
}
return ctx;
}
private Hashtable<String,String> getProperties(String serverUrl, String user, String password)
{
Hashtable<String,String> env = new Hashtable<String,String>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.REFERRAL, "ignore");
env.put("com.sun.jndi.ldap.connect.pool", "false");
env.put("com.sun.jndi.ldap.connect.timeout", "300000");
env.put(Context.PROVIDER_URL, serverUrl);
env.put(Context.SECURITY_PRINCIPAL, user);
env.put(Context.SECURITY_CREDENTIALS, password);
env.put("java.naming.ldap.attributes.binary", "tokenGroups objectSid objectGUID");
return env;
}
}
ActiveDirectoryLdapService
This code basically goes to the AD, using the inputs it gets from the previous class we have just seen. First, it fetches from the AD all the data and stores it in NamingEnumeration<SearchResult>
, which is an enumeration of the search results by the filter. Then, it searches this list for a specific attribute. In the code below, this attribute is the user’s email, and we search it by the property “AD_ATTR_NAME_USER_EMAIL
”. This implementation is just an example, of course, and can vary from one client to another.
To learn more about LDAP filters, read here.
@Component
public class ActiveDirectoryLdapService
{
private static Logger logger = Logger.getLogger(ActiveDirectoryLdapService.class);
private static final String AD_ATTR_NAME_TOKEN_GROUPS = "tokenGroups";
private static final String AD_ATTR_NAME_OBJECT_CLASS = "objectClass";
private static final String AD_ATTR_NAME_OBJECT_CATEGORY = "objectCategory";
private static final String AD_ATTR_NAME_MEMBER = "member";
private static final String AD_ATTR_NAME_MEMBER_OF = "memberOf";
private static final String AD_ATTR_NAME_DESCRIPTION = "description";
private static final String AD_ATTR_NAME_OBJECT_GUID = "objectGUID";
private static final String AD_ATTR_NAME_OBJECT_SID = "objectSid";
private static final String AD_ATTR_NAME_DISTINGUISHED_NAME = "distinguishedName";
private static final String AD_ATTR_NAME_CN = "cn";
private static final String AD_ATTR_NAME_USER_PRINCIPAL_NAME = "userPrincipalName";
private static final String AD_ATTR_NAME_USER_EMAIL = "mail";
private static final String AD_ATTR_NAME_GROUP_TYPE = "groupType";
private static final String AD_ATTR_NAME_SAM_ACCOUNT_TYPE = "sAMAccountType";
private static final String AD_ATTR_NAME_USER_ACCOUNT_CONTROL = "userAccountControl";
public String getUserMailByDomainWithUser(LdapContext ctx, String searchBase, String domainWithUser)
{
logger.debug("trying to get email of domainWithUser " +
domainWithUser + " using baseDN " + searchBase);
String userName = domainWithUser.substring(domainWithUser.indexOf('\\') +1 );
try
{
NamingEnumeration<SearchResult>
userDataBysAMAccountName = getUserDataBysAMAccountName(ctx, searchBase, userName);
return getUserMailFromSearchResults( userDataBysAMAccountName );
}
catch(Exception e)
{
throw new RuntimeException(e);
}
}
private NamingEnumeration<SearchResult>
getUserDataBysAMAccountName(LdapContext ctx, String searchBase, String username)
throws Exception
{
String filter = "(&(&(objectClass=person)
(objectCategory=user))(sAMAccountName=" + username + "))";
SearchControls searchCtls = new SearchControls();
searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);
NamingEnumeration<SearchResult> answer = null;
try
{
answer = ctx.search(searchBase, filter, searchCtls);
}
catch (Exception e)
{
logger.error("Error searching Active directory for " + filter);
throw e;
}
return answer;
}
private String getUserMailFromSearchResults( NamingEnumeration<SearchResult> userData )
throws Exception
{
try
{
String mail = null;
if (userData.hasMoreElements())
{
SearchResult sr = userData.nextElement();
Attributes attributes = sr.getAttributes();
mail = attributes.get(AD_ATTR_NAME_USER_EMAIL).get().toString();
logger.debug("found email " + mail);
}
return mail;
}
catch (Exception e)
{
logger.error("Error fetching attribute from object");
throw e;
}
}
}
Putting It All Together
To use the code above, the user has to call only two methods: createContext()
, and then after getting the context, getUserMailByDomainWithUser()
.
The client-application has to supply the following:
- The URL to the LDAP server
- The credentials to this server (username and password)
- A
String
which is the SearchBase path in the AD - The FQN (fully qualified name) of the user in the AD
In the example below, we are interested in the users’ email only. The first three parameters above are configured per system, hence they are read from a property file. For the purposes of this example, we can hard-code them. The only runtime changeable parameter is the FQN of the user, whose email we are looking for.
The FQN should look like "john\doe”, meaning the domain name is "john
” and the user name is "doe
”.
public class LdapTester
{
@Value("${com.watchdox.kerberos.ad.url}")
private String url;
@Value("${com.watchdox.kerberos.ad.username}")
private String username;
@Value("${com.watchdox.kerberos.ad.password}")
private String password;
@Value("${com.watchdox.kerberos.ad.baseDN}")
private String baseDN;
@Autowired
private ActiveDirectoryConnectionUtils adConnectionUtils;
@Autowired
private ActiveDirectoryLdapService adLdapService;
public void testGetUserMailByDomainWithUser(String fqn)
{
LdapContext ctx = adConnectionUtils.createContext(url, username, password);
String email = adLdapService.getUserMailByDomainWithUser(ctx, baseDN, fqn);
}
}
Performance
The code above opens a connection for each access to AD, which can cause performance issues. This can be tackled in several ways. IMHO, the easiest one to implement is a connection-pool, which can be achieved using the Spring-LDAP package; the details of this are beyond the scope of this document.
Credits
My colleagues Shalom Kazaz and Or Gerson, and special thanks to Mr. David Goldhar.