July 10, 2016

LDAP Authentication and Authorization in a Java Application

You can find many tutorials that describe LDAP, and many that describe Java's APIs for LDAP integration. However, LDAP obviously wasn't designed to perform user authentication/authorization for your app, so there are multiple steps you need to accomplish this. It's not hard, but not intuitive either. That's why this article was written. The pieces were put together by reading the configuration manuals for some (non-Java) products that integrate LDAP for user authentication and role-based authorization. Much time was spent figuring it out. Hopefully this article will help someone else.

LDAP

An LDAP server is a directory service used by companies to centralize corporate information. It contains users, their passwords, and groupings of users, among many other possible things. The LDAP schema defines classes, which are structures (basically, lists of fields) for the data objects.

LDAP holds all the data you need to integrate your application's login screen into a corporate environment. The user/admin benefits because they don't have to remember/manage yet another set of user IDs and passwords just to access your application. You benefit because you don't have to build tools and databases to manage user accounts.

Each user in LDAP will have a unique formal name called a "distinguished name", or DN for short. LDAP uses that DN for all other user references within the database, e.g. in lists of group members. The DN is a long string of hierarchical levels. No one memorizes it. And it may change over time, e.g. if a user changes departments. However, the user usually has a unique 'uid' field too. That 'uid' is what a user will memorize, and that's likely what they'll type into your login screen. (There's nothing too special about 'uid', other than it's a standard field, and supposedly contains unique values. You could just as easily use a 'mail' field, for example.)

Users are usually organized into LDAP groups (of LDAP class 'groupOfNames' or 'groupOfUniqueNames'). A group is basically just a named list of users' DNs. Often, a company's existing groupings may be perfectly applicable to permission roles in your application. So, your application could hopefully be integrated with no LDAP changes needed. That's good, because sometimes there is a "political process" (barrier) between your application's administrators and the company's LDAP administrators. Worst-case, you might need to ask for some new LDAP groups to support your app's permission roles. Technically, that shouldn't be difficult.

Java

Luckily, Java (the "Standard Edition", at least) contains everything you need to integrate LDAP. It's all in the "javax.naming" package and sub-packages. These packages are generalized for other types of directory services too (e.g. file system directories), but you can easily find tutorials that focus on LDAP basics.

How it Works

LDAP authentication is performed in four steps, and a last step for authorization:

  1. You take the user ID (or other identifying info) provided on your login screen, and use it to search LDAP for a single user record that contains that field.
  2. If the search is successful, you can grab the user's DN value from the search result object. If not, the user is unknown, or the user entered the wrong info on your login screen. (If you find multiple search results, then you'll need to think about using a more unique identifier.)
  3. You can close that LDAP connection and attempt a new one, this time using the user's DN, along with the password entered on the login screen.
  4. If that connection is successful, then you know the user authenticated, since their ID and password worked. If not, you can prompt to try again. For applications with all-or-nothing access (i.e. just one role), you're done.
  5. If you need to determine the user's permissions/roles within your application, you will next search LDAP for the user's group memberships. You can associate LDAP's group names to permissions for your app, but that's something you configure in your app, not within LDAP.

LDAP Searches

To perform the initial search for a user's DN, your Java app will need to create a javax.naming.directory.InitialDirContext. The constructor takes a Hashtable (it's old-school; no HashMap). That Hashtable holds some key:value settings, including a DN ("ldap.principal") and password ("ldap.credentials").

To create the first InitialDirContext that's needed to lookup the user's DN, you need to use your LPAD server's read-only access account, unless anonymous searches are allowed. This read-only account is usually shared freely with any users or apps within the company so you'd just put that information in your app's configuration files. (LDAP server's are not usually accessible outside a company's network, although that was apparently a dream of the original designers.)

The LDAP search filter for finding the user's DN looks like: "(uid=john_doe)" Of course, "john_doe" is just an example of what your user might enter at your login prompt.

Once you have the user's DN, you'll need to authenticate the user, then lookup their groups. To authenticate you'll create a new InitialDirContext, but this time putting the user's DN and password in the constructor's Hashtable.

Lastly, you find matching groups (which are LDAP class "groupOfNames") by searching on group's "member" key. (Or "uniqueMember" for groups that are of LDAP class "uniqueGroupOfNames".)

Our particular app has three roles. So we needed to see if the user was a member of three groups. We expose the LDAP filter for the customer to configure, so each filter would match a specific group within their existing LDAP. So the filter matches on a group name, and also the authenticated user's DN. Below is the full JSON-formatted configuration used by the example code. Note that the empty braces {} in the three role filters ("ldap.filter.admin", "ldap.filter.tech", and "ldap.filter.guest") will be replaced by the authenticated user's DN at runtime. The LDAP filter syntax is odd at first glance, in my humble opinion. But the three filters are simply an AND of two tests: if the canonical name field ("cn") of the group matches "Admins" for example, and if the "member" field's value matches the authenticated user's DN.

	{
	  "ldap.url": "ldap://host:389",
	  "ldap.filter.user": "(uid={})",
	  "ldap.filter.admin":"(&(cn=Admins)(member={}))",
	  "ldap.filter.tech":"(&(cn=Techs)(member={}))",
	  "ldap.filter.guest":"(&(cn=Staff)(member={}))",
	  "ldap.context.users": "",
	  "ldap.context.groups": "",
	  "ldap.authentication": "simple",
	  "ldap.principal": "uid=admin,ou=system",
	  "ldap.credentials": "secret"
	}
	

Example Code

Please download the source code for TriLevelLDAP.java. This is actual code from our product, edited only slightly for this tutorial. All the ideas discussed above are illustrated there.

We hope this has been helpful. Please share any feedback or questions.