Contents


Configure multifactor authentication for IBM Cloud Node.js applications

Use risk analysis and enhancements for more security

Comments

In this tutorial, you learn how to configure two-factor authentication for your IBM Cloud Node.js application. Sending a separate token to the user's email address makes masquerading as that user a lot harder. A potential attacker would not only have to steal the password, but would also need to hack into the mail server to get the token.

In addition, this tutorial teaches you some techniques for risk analysis. By analyzing risk, an application can determine when an attempt to log in is risky. It will require a second factor for authentication only in such cases.

What you'll need to build your application

Run the appGet the code

In this tutorial, you learn how to use a random string delivered by email as a second authentication factor. I also discuss several methods for risk analysis.

Why use two-factor authentication for risky logins?

Passwords have two main failure modes:

  • Inadvertent disclosure: An attacker discovers the password of an authorized user.
  • Password sharing: An authorized user gives the password to somebody else, usually to enable that person to use some of the authorized user's permissions.

Both of these failure modes can be fixed by requiring authorized users to prove that they have access to their email as a second authentication factor. The second factor can be required each time, or only when a transaction appears dangerous and requires extra security.

Get started

Follow these steps to create a new Node.js application on IBM Cloud:

  1. Log on to IBM Cloud. Create a free account if you do not have one.
  2. Click on the menu icon and select Cloud Foundry Apps. this is how you get to the platform as a service (PaaS) offerings in IBM Cloud.
  3. Click Create Cloud Foundry app.
  4. Click SDK for Node.js.
  5. Type an app name (for example, mfa-app) and a hostname (I chose mfa-app, so you will have to choose something else). Then click Create.
  6. Wait for the application to start.

Configure the web IDE

We could develop the application on our own system, but I prefer the web-based IDE.

  1. When the application page opens, click Overview on the left sidebar, scroll down to the Continuous delivery heading and click Enable.
  2. Scroll down and specify the repository type Clone and the source repository URL https://git.ng.bluemix.net/qbzzt1/mfa-app,
  3. Click Create.
  4. Once the tool chain is created, click Eclipse Orion Web IDE to edit the application files.
  5. Open manifest.yml and change the name and host from two-factor authentication to the values you entered when you created the application (step 5 under ).
  6. Click the play button icon to deploy the application with the new manifest.

Authentication

First, you need an authentication workflow that uses the second factor authentication. To do this, you send emails with long random strings. Only the legitimate user, or others with access to that user's email, can access these values.

Create random strings

The easiest way to create long random strings is to download and use the node-uuid package, which creates RFC 4122 identifiers. Each of those identifiers has 60 random bits, which are enough for all practical purposes. The identifiers are encoded as text. These are the steps to create a UUID object (you can also see them in the source code).

  1. To use node-uuid, add a dependency on node-uuid (at any version) to package.json:
    "dependencies": {
        "express": "4.12.x",
        "cfenv": "1.0.x",
        "body-parser": "*",
        "node-uuid": "*"
    	},
  2. Then, create a uuid object:
    // Use uuid to generate random strings.
    var uuid = require("node-uuid");

Send out messages

To send out email messages, use the SendGrid service in IBM Cloud. First, you need to create and bind the service and obtain an API key:

  1. Log in to IBM Cloud and click on your application in the dashboard.
  2. Click Overview on the left sidebar.
  3. Scroll down to the Connections and click Connect new.
  4. Select the Application Services category of the catalog and click the SendGrid service.
  5. Select a package and click Create.
  6. If you are prompted, click Restage.
  7. Click the menu icon and select Application Services. Then click on the SendGrid service that you created.
  8. Click Open SendGrid Dashboard.
  9. From the SendGrid site, click Settings > API Keys on the left sidebar.
  10. Click Create API Key.
  11. Name the key, select Full Access, then click Create and View.
  12. Copy this API key to the clipboard:

    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

Use SendGrid to send an email

  1. Return to the IBM Cloud console. Go to the application that you created, scroll down, and click View toolchain.
  2. Add a dependency on SendGrid (at any version) to package.json (which is already done in the sample application):
    "dependencies": {
        "express": "4.12.x",
        "cfenv": "1.0.x",
        "body-parser": "*",
        "node-uuid": "*",
        "sendgrid": "*"
    	},
  3. Create a SendGrid object by using the API key that you received and use it to send email. Do it once from the main code of app.js, rather than a handler, to verify that everything works.

    Note: When you start by cloning the application, you just need to change the API Key in app.js, on line 41.

    // Use SendGrid to send emails as a second token.
    var sendgrid = require("sendgrid")("API_KEY goes here ");
      
    // Send an email
    var email = new sendgrid.Email();
    
    email.addTo("unmonitored@my.app");
    email.setFrom("qbzzt1@gmail.com");
    email.setSubject("");
    email.setHtml("<H2>Big test</H2>");
    
    sendgrid.send(email);

You should receive the email in a minute or two. If not, make sure to look in your spam folder. Many filters consider email like this to be spam.

Putting it all together: The authentication workflow

Users register and log in by filling out different forms on index.html. Their information is sent in a POST request to the server. The rest of this section explains the login flow; the registration flow is very similar.

The user attempts to log in

First, the code checks if the email and password pair is even valid. The users are stored in a hash table, where the key is the user's email address. If the user does not exist, or if the password is wrong, the application returns to the user an error message. It is the same message whether the user is nonexistent or the password is wrong. This avoids unintentionally revealing whether an email address belongs to a valid user.

var user = users[req.body.email];
  
if (!user) {
  	res.send("Bad user name or password.");
  	return ;
}
  
if (user.password !== req.body.passwd) {
  	// Same response, not to disclose user identities
  	res.send("Bad user name or password.");
  	return ;  	
}

Note that storing users' records in a hash table like this is simple, and is therefore ideal for sample programs such as this one. But it is not a good idea to delete all the users whenever you restart the application in production, or to have different instances of the application have different user lists. In production, you should use the Cloudant DB.

If the user and password match, check if the user is still pending. If so, this is also an error condition. You can add a link to resend the confirmation email to the message sent to the user.

// User exists, but email not confirmed yet
if (user.status === "pending") {
  	res.send("Account not confirmed yet.");
  	return ;
}

Assuming that everything checks out, the next step is to create a request.

// Create request to confirm the logon
var id = putRequest(req.body.email);

The putRequest function starts by creating a random identifier as explained earlier.

// Register a pending request for this email
var putRequest = function(email) {
   // Get the random identifier for this request
   var id = uuid.v4();

Next, it adds the request to the pendingReqs hash table with that identifier. The request includes the identity of the requesting user. It also gets a time stamp, to allow you to clean up old requests that are abandoned. As noted above in regard to the email/password pair, in a production application, the pendingReqs hash table should be a database instead.

   pendingReqs[id] = {
   	email: email,
   	time: new Date()
   };

The function that called putRequest needs to inform the user of the ID so the user can verify that the request is legitimate. Therefore, putRequest returns the ID to the caller.

   return id;
};

The application sends a token by email

After putRequest, the handler calls a function to send the user an email and responds to the user.

  // E-mail the account confirmation request
  sendLoginRequest(req.body.email, id);
  
  res.send("Thank you for your request. Please click the link you will receive by email to " +
    req.body.email + " shortly.");	
});

The sendLoginRequest function composes an HTML message and sends it to the user. There are two variables in the message text. The first, appEnv.url, is the URL used to access the application. This is necessary because a relative link won't work in an email that doesn't have the context of the web browser's last URL. The second is the ID of the request to be approved. Taken all together, the URL in the message is <appEnv.url>/confirm/<id>. This is the URL where you will get the confirmation if the email address is correct.

// Send a link. Standard practice is to send a code, but using a link
// is easier and more secure.
var sendLoginRequest = function(email, id) {
  
  // Send an email
  var msg = new sendgrid.Email();

  msg.addTo(email);
  msg.setFrom("notMonitored@nowhere.at.all");
  msg.setSubject("Application log in");
  msg.setHtml("<H2>Welcome to the application</H2>" +
  	'<a href="' + appEnv.url + '/confirm/' + id + '">' +
  	'Click here to log in</a>.');
  	
  sendgrid.send(msg);

};

Note that this is different from the standard practice, which is to provide a short (4-6 characters) code in the email for the user to type into a web form. I prefer this method because it is easier and allows for more possible keys. The disadvantage is that anybody who can access the email can break into the application. In the "Keep safe from email sniffers" section near the end of this tutorial, I discuss how to solve that problem.

The user logs in with the emailed token

The email directs the user to a URL at the path confirm/<id>. This call is processed by the code below. The :id string means it can be any valid path component, and the value will be available in req.params.id.

// A confirmation (of an attempt to register or log in)
app.get("/confirm/:id", function(req, res) {

The first thing to do is to retrieve the request that is being confirmed and delete it. If there is no such request, report the error to the user.

	var userRequest = pendingReqs[req.params.id];
	delete pendingReqs[req.params.id];

    // Meaning there is no user request that matches the ID.
    if (!userRequest) {
    	res.send("Request never existed or has already timed out.");
    	return ;   // Nothing to return, but this exits the function    	
    }

The object for every request includes the email address that identifies the user. This lets you retrieve the user information.

	var userData = users[userRequest.email];

If the user is pending, it means that this is a confirmation of the account.

if (userData.status === "pending") {
    userData.status = "confirmed";
        res.send("Thank you " + userRequest.email + " for confirming your account.");    	
		return ;
}

If the user account is already confirmed, then this is a confirmation of the second factor for authentication.

	// In a real application, this is where we'd set up the session and redirect
	// the browser to the application's start page.
	res.send("Welcome " + userRequest.email);
});

In a real application, this is where you would create a session and put a session cookie in the browser. To learn how to do that on Node.js, refer to the tutorial "Use LDAP and Active Directory to authenticate Node.js users."

Note: This account is somewhat simplified. When SendGrid receives an email to send, it replaces the links with links to its own site, where the browser is redirected to the original URL. This allows SendGrid to provide statistics for links accessed through email. In the case of the following illustration, you see that on Thursday, SendGrid sent 13 messages, and got nine clicks, to seven unique URLs.

Screen shot showing statistics     overview
Screen shot showing statistics overview

Risk analysis

It is possible to require two-factor authentication every time a user logs in. However, that is considered user hostile. It is much better from a usability perspective if the application evaluates the chance that a login attempt is illicit and use that information to decide whether requiring a second factor is warranted.

It is important that this decision be based on factors that are difficult to forge. For example, the type and version of the browser is very easy to fake in an HTTP header. It is much harder, though, to fake IP addresses (because you need the response routed to you) or the time of access.

Client IP address

Browsers do not access IBM Cloud directly, but through IBM WebSphere DataPower Appliances acting as proxies. To obtain the client IP address, rather than that of the proxy, the application has to trust the proxy. You set this using app.set:

// Necessary to know the IP of the browser
app.set("trust proxy", true);

The IP address from which a request arrives is available in req.ip. Here it is in use:

// Show the user's IP address
app.get("/ip.html", function(req, res) {
	res.send("<H2>Your IP address is</H2>" + req.ip);
});

To see the result, browse to http://two-factor-auth.mybluemix.net/ip.html.

Interpreting the IP address

To use the IP address, you need to interpret it. One easy-to-use database of IP addresses is http://ipinfo.io. You can go to http://ipinfo.io/<ip address> to get complete information, or http://ipinfo.io/<ip address>/<field> to get a specific field, such as the country.

To learn how to send an HTTP request and receive a response from the application, see Step 3 in "Build a self-posting Facebook application with IBM Cloud and the MEAN stack, Part 3." Here is the code used in this application:

// The library to issue HTTP requests
var http = require("http");

Because Node.js is single threaded, and the result will be available only after the request gets to ipinfo.io and the response comes back, use a next() function that is called when the result is available.

// Interpret an IP address and then call the next function with the data
var interpretIP = function(ip, next) {

The http.get function receives a URL and a callback function. It then gets the URL from its server.

	http.get("http://ipinfo.io/" + ip,

This callback function is called as soon as you get the HTTP headers. But the data you need is provided in the HTTP body of the response. Therefore, you need to wait until you receive data.

		function(res) {

This code registers a handler for a data event. Because the response is so short, it can be assumed to come in a single chunk. If there were multiple chunks, you would concatenate them together until you got an end event.

			res.on('data', function(body) {

When you don't access it from a browser, ipinfo.io helpfully provides the data in a JSON object, which is easy to parse.

				var data = JSON.parse(body);
				next(data);
			});
		}
	);

};

// Show the user's IP address
app.get("/ip.html", function(req, res) {
	interpretIP(req.ip, function(ipData) {
		var resHtml = "";
		resHtml += "<html><head><title>IP interpretation</title></head>";
		resHtml += "<body><H2>Intepretation of " + req.ip + "</H2>";

To show the result, put all the data fields in a table.

		resHtml += "<table><tr><th>Field</th><th>Data</th></tr>";
		for (var attr in ipData) {
			resHtml += "<tr><td>" + attr + "</td><td>" + ipData[attr] + "</td></tr>";
		}
		resHtml += "</table></body></html>";
		res.send(resHtml);
	});
});

To see the result for your own IP address, browse to https://two-factor-auth.mybluemix.net/ip.html.

Time and day of the week

Getting the time and day of the week is very simple. Just create a new Date object. The days of the week start with 0 as Sunday and 6 as Saturday; the hour is 0-23. However, the time zone is UTC, the London time zone (without daylight savings time). This means, for example, that for CST in the US you need to deduct 6 hours.

Usually, the risk depends on whether the time can be classified as business hours, evening, or weekend. This is the code that handles that:

// Classify time as "day", "after hours", or "weekend". The time zone
// is the difference in hours between your time and GMT.
var classifyTime = function(timeZone) {
	var now = new Date();
	
	// Hour of the week, zero at a minute after midnight, on Sunday
	var hour = now.getDay()*24 + now.getHours() + timeZone;

	// If the hour is out of bounds because of the time zone, return it
	// to the 0 - (7*24-1) range.
	if (hour < 0)
		hour += 7*24;
	
	if (hour >= 7*24)
		hour -= 7*24;
		
	// The weekend lasts until 8am on Monday (day 1) and starts at 5pm on
	// Friday (day 5)
	if (hour < 24+8 || hour >= 5*24+17)
		return "weekend";
		
	// Work hours are 8am to 5pm
	if (hour % 24 >= 8 && hour % 24 < 17)
		return "day";
	
	// If we get here, it is after hours during the work week
	return "after hours";
};



// Show the current time and day of the week
app.get("/now.html", /* @callback */ function(req, res) {
	var now = new Date();
	
	var resHtml = "";
	resHtml += "<html><head><title>Present Time</title></head>";
	resHtml += "<body><H2>Present Time</H2>";
	resHtml += "Day of the week (UTC): " + now.getDay() + "<br />";
	resHtml += "Hour (UTC): " + now.getHours() + "<br />";
	resHtml += "Time classification CST:" + classifyTime(-6) + "<br />";
	resHtml += "</body></html>";
	
	res.send(resHtml);
});

To see the current result for CST click here.

Show an example of risk analysis

The problem with using risk analysis in a sample application is that it can be an annoyance to check the parameters. You would want to see the results for multiple countries and multiple times, without traveling or waiting. Therefore, the risk page lets you manually specify the time classification and the IP address.

Screenshot of risk page
Screenshot of risk page

Risk analysis policy

Using two parameters—IP address and time classification—you can set up a policy to decide what to do. For example, you might decide that logins from the US are expected only during business hours, logins from China are expected at any time except the weekend (because their working hours are very different), and you never expect users to log in from anywhere else.

It is trivial to implement such a policy in code:

// Decide the risk level
app.post("/risk", function(req, res) {
	interpretIP(req.body.ip, function(ipData) {
		var country = ipData.country;		
		var time = req.body.time;
		var resHtml = "";
		var safe = false;
		
		resHtml += "<html><head>";
		resHtml += '<link rel="stylesheet"  ' +
			'href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">';
   		resHtml += '<link rel="stylesheet" ' +
			'href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/' +
			'css/bootstrap-theme.min.css">';
   		resHtml += '<script ' +  	
			'src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js">' +
			'</script>';
   		resHtml += "</head><body>";
		
		resHtml += "<H2>Risk Level:</H2>";
		resHtml += "Country: " + country + "<br />";
		resHtml += "Time classification: " + time + "<br />";
		
		// Only expect log in during work hours from the US
		if (country === "US" && time === "day")
			safe = true;
			
		// Log ons from China are expected at any time except weekends
		if (country === "CN" && time !== "weekend")
			safe = true;		
		
		if (safe)
			resHtml += '<span class="label label-pill label-success">' +
				'User name and password</span>';
		else
			resHtml += '<span class="label label-pill label-danger">' +
				'Two factor authentication</span>';
		
		resHtml += "</body></html>"
		
		res.send(resHtml);
	});	
});

To apply this policy, simply calculate the value of safe in the login handler, and add an if statement for the next step.

  if (safe) {
	createSession(user, res);
  } else {
 
	// Create request to confirm the logon
	var id = putRequest(req.body.email);
  
	// E-mail the account confirmation request
	sendLoginRequest(req.body.email, id);
  
	res.send("Thank you for your request. Please click the link you will receive by email to " +
	req.body.email + " shortly.");	
  }

Enhancements

There are several enhancements that can improve this program, making it safer and more stable.

Keep safe from email sniffers

There is a security problem, mentioned above, because any attacker who can get the user's email can break into the application by using the confirmation link. One solution uses browser cookies. First, add cookie-parser to package.json and use it in app.js:

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

Then, modify the login handler to:

  1. Generate a second random ID.
  2. Place that random ID in a browser cookie.
  3. Place the same random ID in the pending requests structure along with the user's email address.
  // For preventing somebody who gets the email from logging on:
  var id2 = uuid.v4();    // 1  
  pendingReqs[id].cookie = id2;   // 2
  res.setHeader("Set-Cookie", ['secValue=' + id2]);  // 3

Also, modify the confirmation link handler to retrieve the value of the cookie that is created in the login handler and compare that value to the value in the pending request. If the values are not the same, the login fails.

    // For preventing somebody who gets the email from logging on:
	if (req.cookies["secValue"] !== userRequest.cookie) {
		res.send("Wrong browser");
		return ;
	}

To verify that this works, log in from one device and then click the confirmation email from another device, or from another browser on the same device. This should fail.

Cleanup

Right now, if users do not click the link for some reason, the pending request just stays active, taking up memory and increasing the time that it takes to look up active requests.

To solve this, use the setInterval function to delete old requests. JavaScript measures time in milliseconds, so to get 5 minutes it is necessary to multiply 5 by 60,000.

// Delete old pending requests
var maxAge = 5*60*1000; // Delete requests older than five minutes

// Run this function every maxAge
setInterval(function() {
	var now = new Date();
	for (var id in pendingReqs) {   // For every pending request
		if (now - pendingReqs[id].time > maxAge)   // If it is old
			delete pendingReqs[id];   // Delete it
	}

Because the cleanup function runs every 5 minutes, pending requests are deleted between 5 and 10 minutes after they are created.

}, maxAge);
Debugging

To debug the cleanup function, it is useful to know the value of pendingReqs. This call makes it available from a browser. (Note: Remember to delete this function before the application is deployed in production. It discloses the two values that can be used to break into the application.)

app.get("/pend", /* @callback */ function(req, res) {
	res.send(JSON.stringify(pendingReqs));
});

The /* @callback */ comment above does not change the function. Its purpose is to tell the editor that even though req is not used anywhere, it is required because it is a callback function and you do not determine which parameters it gets. This removes the warning and makes it easier to focus on potential problems.

Screen shot showing parameter not used message
Screen shot showing parameter not used message

Require HTTPS

It is a bad idea to allow users to submit passwords and respond with cookies in clear text. Add this call to redirect HTTP users to HTTPS. Put it before any other handler declaration for the app.

//Handle all (any method) and any path (slash followed by any string)
app.all('/*', function(req, res, next) {

The application always gets HTTP, because the SSL tunnel is terminated by IBM WebSphere® DataPower. However, the original protocol is available in the header as x-forwarded-proto.

	// If the forwarded protocol isn't HTTPS, send a redirection
	if (req.headers["x-forwarded-proto"] !== "https")
		res.redirect("https://" + req.headers.host + req.path);
	else

The third parameter of the callback (for any part of the app.<HTTP method> functions, not just app.all), is the function to call if this callback does not handle the request. If the request is already HTTPS, you do not need to redirect and therefore you let normal processing resume.

		next();
});

SMS instead of email

The Internet was not built with security in mind. The telephone network, on the other hand, was. It is therefore safer to send tokens with SMS instead of email. To do so:

  1. Add a mobile phone number field to the registration.
  2. Instead of a long token, create a short one that people can type. For example: uuid.v4().substring(0,5)
  3. Use the Twilio service in IBM Cloud to send the tokens by SMS.
  4. Instead of telling users to click the confirmation link, redirect them to a form where they can type the token.

User profiles

Instead of treating all users as identical, it is possible to store some profile information, such as the user's role or normal location, and include that information in the risk analysis. For example, John Doe typically logs in from the US. When he logs in from China, that might be suspicious and require a second factor. But when Chang Xiu, an employee in China, does it, it is not suspicious. The converse is true when Joe and Chang both log in from the US, or when Chang logs in when it is noon Central time, which would be 2 AM for him.

Conclusion

You should now be able to implement two-factor authentication in your IBM Cloud Node.js applications. You should also be able to use risk analysis to identify risky cases where it makes more sense to deploy two-factor authentication.


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, Cloud computing
ArticleID=1026851
ArticleTitle=Configure multifactor authentication for IBM Cloud Node.js applications
publish-date=03162018