Contents


Use LDAP and Active Directory to authenticate Node.js users

Provide authentication and authorization decisions for a Node.js application

Comments

If you already have an internal IT infrastructure, it quite likely contains an LDAP server (possibly Active Directory, acting as an LDAP server) to serve user identities. In many cases, it is best to continue to use that directory, even when your application sits in the IBM Cloud. In this tutorial, I show you how to authenticate users.

What you'll need to build your application

Run the appGet the code

What is LDAP?

LDAP (light-weight directory access protocol) is an Internet standard. In addition to the protocol used to access the directory, LDAP defines the naming convention that's used to identify entries and the schema that specifies the information included in them.

The naming convention

Entries in LDAP are stored in a tree, called the directory information tree. The root of the tree is called the suffix, and the branches are containers. Those containers can be organizational units, locales, and more. The leaves of the tree are the individual entities.

You can see an example of this structure in the following image. The suffix is o=simple-tech. Under it there are the branches: ou=people (which is for users) and ou=groups (which is for groups). Under the users' branch, there are two entities that refer to individual users: uid=alice and uid=bicll.

Figure 1. Sample directory information tree
Chart showing LDAP directory information tree
Chart showing LDAP directory information tree

To obtain the distinguished name (DN), the full identifier for an entity, start at the entity itself and go up the tree, gathering all the identifiers and separating them with commas. For example, the distinguished name for alice is uid=alice,ou=people,o=simple-tecch.

The schema

The schema specifies the attributes, the information that is stored about each entity. One attribute that every entity has is objectClass, which specifies what type of entity it is. In most cases, user information is stored as the object class inetOrgPerson and groups are stored as groupOfNames. In every object class, some attributes are mandatory and some are optional. For example, in inetOrgPerson, the attributes for a common name (cn) and surname (sn) are mandatory. Other attributes, such as those for user ID (uid) and password (userPassword), are optional.

Active directory

Microsoft™ Active Directory has an LDAP interface. However, as in many other areas, Microsoft has a different interpretation of the standard. Here are the differences that are relevant to logging users in.

LDAPActive Directory
User identiferuidEither sAMAccountName (user identifier only) or userPrincipalName (<user identifier>@<domain>)
Bind identifierdistinguished nameYou can use the distinguished name if you know it, but you can also use userPrincipalName.
SuffixMany options (domain components is one of them) Always the domain name as dc (domain component) attributes. For example, for ibm.com, the suffix would be dc=ibm,dc=com

Logging users into an application

Accessing an internal server from IBM Cloud

Obviously, the application on the IBM Cloud needs to be able to access your internal LDAP (or Active Directory) server. In theory, you could just open the LDAP server to the Internet—but that violates the principle of least privilege. You shouldn’t allow access that isn’t necessary.

A better solution is the use the IBM Cloud’s Secure Gateway service. You install a small client on your internal network, and it tunnels the LDAP traffic between the IBM Cloud and the server in your data center. You can install it on Windows™, Linux®, MacOS, or as a Docker.

Logging on to an LDAP server

Verifying credentials with LDAP is typically a two-step process. First, the program needs to access the LDAP server as a user with read and search privileges on the users to get the user information, including the user's distinguished name (DN). Then, it needs to attempt to access the server as the user's DN, using the provided password. You can see this process starting in line 32 of app.js.

The log on process is in the callback for a POST request to /ldap. The application has middleware that parses such requests, which are provided as JSON.

app.post("/ldap", (req, res) => {
	var result = "";    // To send back to the client

The first step is to create an LDAP client by using the server URL. In this application the server URL comes from the user, which allows you to try to log on to your own LDAP server.

	var client = ldap.createClient({
  		url: req.body.serverUrl
	});

The LDAP client needs to bind, which is the LDAP term for authenticating. This requires a long round trip, so the rest of the flow is in a function that is provided as a parameter to the bind function. The same is true for the other LDAP operations. Note that in a real-life application the server URL, reader DN, and reader password are all part of the application. The only reason that they are provided by the user here is to allow the use of different LDAP servers.

	client.bind(req.body.readerDN, req.body.readerPwd, function(err) {
		if (err) {

If the bind fails, return a result that informs the user of the problem. Then, return to leave the function.

			result += "Reader bind failed " + err;
			res.send(result);
			return;
		}
		
		result += "Reader bind succeeded\n";

The LDAP schema specifies that if a user ID is available, it is stored in a uid attribute. You can check for it by using this syntax: (<attribute>=<value>). This code builds the filter and uses it in a search.

		var filter = `(uid=${req.body.username})`;

Add the filter to the result to show the user, for debugging purposes.

		result += `LDAP filter: ${filter}\n`;

In addition to the filter, an LDAP search needs to know where to start (the suffix or a branch under it) and the scope—that is, how deep to search. The scope below—sub—means that there is no depth limit. A scope of one would specify that only entities that are directly below the starting location should be included in the search.

		client.search(req.body.suffix, {filter:filter, scope:"sub"},
			(err, searchRes) => {

Every time an entry is returned from the search, it will be in a separate callback. This array allows us to put all those entries together (there should be only one, but it is best to check in case of error). There is no concurrency problem because of the single threaded nature of Node.js.

				var searchList = [];
				
				if (err) {
					result += "Search failed " + err;
					res.send(result);
					return;
				}

Here is where we add entries we find to the searchList array.

				searchRes.on("searchEntry", (entry) => {
					result += "Found entry: " + entry + "\n";
					searchList.push(entry);
				});

We are unlikely to get an error at this stage, but if we do report it.

				searchRes.on("error", (err) => {
					result += "Search failed with " + err;
					res.send(result);
				});

This is the callback when the search is done.

				searchRes.on("end", (retVal) => {
					result += "Search results length: " + searchList.length + "\n";
					for(var i=0; i<searchList.length; i++) 
						result += "DN:" + searchList[i].objectName + "\n";
					result += "Search retval:" + retVal + "\n";

If there is exactly one entry, try to bind to it.

					if (searchList.length === 1) {
						client.bind(searchList[0].objectName, req.body.password, function(err) {
							if (err) 
								result += "Bind with real credential error: " + err;
							else
								result += "Bind with real credential is a success";

Respond to the user with the text we accumulated as the result.

							res.send(result);	
						});  // client.bind (real credential)

If the number of search results is not one, there is a problem.

					} else { // if (searchList.length === 1)
						result += "No unique user to bind";
						res.send(result);
					}

				});   // searchRes.on("end",...)
				
		});   // client.search
		
	}); // client.bind  (reader account)
	
}); // app.post

Testing the LDAP log on

Click here for a front end to the application. If you have a publicly accessible LDAP server, you can use it. If not, zFlex Software has kindly provided the world with a publicly accessible instance of IBM Security Directory Server (SDS). The default values in the front end are to access that server.

Note that you cannot use the Secure Gateway Service with this application because you would need administrator privileges to bind the service with the application. I am not going to give everybody administrative privileges on my IBM Console account. If you want to test that, just copy the files from GitHub and upload them into your own application.

Logging on to an Active Directory server

You could follow the same procedure to log on to Microsoft Active Directory, but there is no need. Instead of binding with the distinguished name, which users typically do not know, you can bind with a user name and a domain. You can see this process in app.js, starting at line 106.

You can use the same front-end application to log on to Active Directory. Unfortunately, I couldn’t find any publicly accessible servers to use, so you’d have to use it with your own one.

Roles-based access control

In most applications, different users have different permissions. Users get permissions based on their role, the actions they are supposed to do on the application. In LDAP, roles are usually encoded as user groups.

LDAP

Group objects typically have the object class groupOfNames and a member attribute that is multi-valued. Multi-valued attributes can have multiple values, in this case the DNs of all the members of the group.

To get the groups that have a specific user as a member, we need the user’s DN. We already get that value as part of the log on process, so all we need is to add another LDAP search to look for entries with that member attribute. You can see the code that does this in app.js, starting on line 173. The search is nearly identical to the search for the user identifier earlier, except that having multiple returned value is legitimate. There is nothing inconsistent in a user that has multiple roles and is a member of multiple groups.

Active Directory

In Active Directory, getting the groups is even simpler. The user object has an attribute, memberOf, which has a value for every group of which the user is a member. After you bind with the user credentials, all you need to do is read the user object (users are allowed to read this own object) and get that attribute.

You can see code that implements this in app.js, starting with line 243. The way ldapjs returns values is a bit complicated (to support multi-value attributes, binary attributes, and so on). Let’s go over the lines that process the user entry, 252-257. The entry is provided to a callback function:

searchRes.on("searchEntry", (entry) => {

The entry is an object that has multiple attributes. The most important ones for our purpose are objectName, the DN of the entry, and attributes, its attributes. The way the attributes are provided is a bit strange, as it is an array of objects with each object having two fields: type for the name of the attribute and vals for its value (or values if it is a multiple value attribute) in an array. You can see an example in Figure 2.

Figure 2. Part of a user entry
userentry
userentry

To get only those array entries that have a specific type, for example memberOf. You can use the filter function. It returns only those entries for which its parameter function is true.

var lst = entry.attributes.filter((x) => x.type === "memberOf");

There should be exactly one value in the list after it is filtered. If for some reason there aren’t any, don’t do anything to avoid an error condition.

if (lst.length)

The vals contain the array of groups of which the user is a member.

		groups = lst[0].vals;	
});

Session Management

We do not want to ask users for authentication and go through the entire resource-intensive process of LDAP authentication every time the user accesses a page. It makes a lot more sense to store the user information somewhere and retrieve it as needed.

Note: The test application for this article does not have this functionality because it focuses on showing the LDAP interaction.

Global definitions

To have sessions, we create an empty hash table in a global variable. This works because we expect to only have one instance of the process. If an application might need to run in multiple instances to serve heavy demand, it might be necessary to use a database and just cache sessions in a local variable.

// Current session information
var sessions = {};

To be able to use session information, import and use the cookie parser middleware:

// Use cookie-parser to read the session ID cookie
var cookieParser = require("cookie-parser");
app.use(cookieParser());

Log on process

When we identify a user as legitimate, we create a session number, which needs to be random, so attackers won’t be able to guess it. Any attacker that can guess this value can pretend to be the legitimate user.

var sessionID = uuid.v1();

We put whatever information we need to store about the user (DN, group membership, and more) in the sessions hash table.

sessions[sessionID] = sessionData;

The session ID is placed in a cookie in the browser for future use and the user is redirected to a web page with the real content.

res.setHeader("Set-Cookie", ["sessionID=" + sessionID]);
res.redirect("main.html");

Using sessions

To use session information, read the cookie and use it as a key to the sessions hash table. If you can’t find the session, have the user log on again.

if (sessions[req.cookies.sessionID] != undefined)
	sessionData = sessions[req.cookies.sessionID];
else
	res.redirect("login.html");

Timeouts

You cannot allow sessions to accumulate. If the application runs for a long time, the sessions object will grow to be unwieldy and waste RAM. This is a very simple algorithm that deletes old sessions without costing too much in memory or CPU. If it is acceptable to delete sessions after an hour, you can use setInterval to look for old sessions once an hour. To do this, go over the session list, and if any session has the old flag (it has a field that is called "old", and it has a true value), delete it. If it does not, create that field and set it to true. Use the setInterval function to do this every hour.

Because we run the operation once an hour, a session could get the "old" flag when it is only 1 second old, or it might exist without the old flag for almost an hour. However, each session stays with the old flag for one hour, so no session is deleted before it reaches an hour, and no session survives past 2 hours.

var sessionLifetime = 60;   // In minutes
setInterval(function() {
    for(var sessionID in sessions)
        if (sessions[sessionID].old)
            delete sessions[sessionID];
        else
            sessions[sessionID].old = true;     
}, sessionLifetime * 60 * 1000);

Note that in a production application, sessions are usually preserved until the user becomes inactive. To do that when you are using this algorithm, simply set the old flag to false whenever a session is used.

Conclusion

Using the techniques that are explained in this tutorial, you will be able to use an internal user repository with an LDAP interface, such as IBM Security Directory Server or Microsoft Active Directory, to provide authentication and authorization decisions for a Node.js IBM Cloud application—or any Node.js application—that can access the LDAP server.


Downloadable resources


Related topics


Comments

Sign in or register to add and subscribe to comments.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Security
ArticleID=1023672
ArticleTitle=Use LDAP and Active Directory to authenticate Node.js users
publish-date=02072018