Single Device Session Handling Using NodeJS

Swarnendu De July 1, 2016

 

Want to build an app with node.js that is secured and can be trusted by the user; seems you need to handle sessions…!!

What is Session…??

In the computing world, a session refers to a limited time of communication between two systems. It is a sequence of interactions between client and server, or between user and system. It’s the period during which a user is logged in or connected.
A session is established through a login procedure and terminated by a log-out procedure. A network administrator might limit the duration of a session, at the expiration of which the session will be timed-out, or terminated. A session may also timeout if no activity is detected for a preset period of time.

“Why to use session and what happens if not employed?”

Now as we have seen what is a session, how it is created and maintained. But now – a question arises why to use sessions?

This could be best answered by explaining what will happen if a developer does not use sessions in his app or website. Following are few problems developers face if they do not have session handling functionality in their applications:

  • Identifying users uniquely would be difficult
  • Storing the application current state for a user in server is a hefty task
  • This will make your user accounts vulnerable to thefts and threats .
  • That why we need to employ session as a functionality while developing applications.

There are many different NPM modules for handling and maintaining the sessions like passport, oAuth and express, these are very secured and enhanced with various other important functionalities but today we are going to try and handle this session by some custom build module which can be used according to your need.

Case Study:

Nowadays we all come across applications which demand that a single user can only log in from a single device at a time ie. a user will be logged out of all devices as soon as he tries to log in from certain another device; Just as is the case with Whatsapp.

In this blog, we will try to solve this scenario. Here are a few steps to help you –

Login:

  • Whenever a user will provide his/her credentials in the login screen of the app, check whether the user already exists in the database.
  • If yes, then match his credentials with the one that is stored in the database.
  • Now if this credentials also match then check whether the user exists in the “session_custom” table of the database.
  • The main play is here, if the user is found in this “session_custom” table remove his previous stored data from here to get him forced logout of all the other devices and store the user id with his new device id, device type, and random token with a destroy time(specifies the time for how long user will be allowed to be logged in the device before his session expires) and if the user session does not exist in the database just save his new data.
exports.login = function(req, res, next) {
  req.app.utility.async.waterfall([
    function(cb) {
      // checking for the existence of the email and convert it into lower case
      if (!req.body.email) {
        req.workflow.outcome.errfor.email = req.app.utility.message.REQUIRED('email');
        return cb('custom');
      }
      req.body.email = req.body.email.toLowerCase();
      // checking the validation for email and password
      if (!req.app.config.User.emailRegExp.test(req.body.email)) {
        req.workflow.outcome.errfor.email = req.app.utility.message.INVALID('email');
        return cb('custom');
      }
      if (!req.body.password) {
        req.workflow.outcome.errfor.password = req.app.utility.message.REQUIRED('password');
        return cb('custom');
      }
      // checking for the existence of device Id and device Type in the request header
      if ((!req.headers['x-auth-deviceid']) && (!req.headers['x-auth-devicetype'])) {
        req.workflow.outcome.errfor.device = req.app.utility.message.REQUIRED('deviceType and deviceId');
        return cb('custom');
      }
      // checking for the existence of the user in the User collection
      req.app.db.models.User.findOne({
        email: req.body.email
      }, function(err, data) {
        if (err) {
          return cb(err);
        }
        // checking the user's credential
        if (!data) {
          req.workflow.outcome.errfor.credential =
            req.app.utility.getLang('INVALID_CREDENTIAL', req.query.lang);
          return cb('custom');
        }
        req.app.utility.validatePassword(req.body.password, data.password,
          function(err, match) {
            if (err) {
              return cb(err);
            }
            if (!match) {
              req.workflow.outcome.errfor.credential =
                req.app.utility.getLang('INVALID_CREDENTIAL', req.query.lang);
              return cb('custom');
            }
            return cb(null, data);
          });
      });
    },
    function(user, cb) {
      //searching the user in the Session collection
      req.app.db.models.SessionCustom.remove({
        user: user._id
      }, function(err) {
        if (err) {
          return cb(err);
        }
        /*generate a new access token for new user and save the details in the 
Session collection*/
        var expiredTime = new Date();
        expiredTime.setMinutes(expiredTime.getMinutes() +
          req.app.config.User.sessionExpiredTime);
        var randomToken = req.app.utility.generateUniqKey();
        var newSessionCustom = new req.app.db.models.SessionCustom({
          user: user._id,
          deviceId: req.headers[['x-auth-deviceid']],
          deviceType: req.headers[['x-auth-devicetype']],
          token: randomToken,
          destroyTime: expiredTime
        });
        newSessionCustom.validateKeys(function(err) {
          if (err) {
            req.workflow.outcome.errfor = err;
            return cb('custom');
          } else {
            newSessionCustom.save(function(err) {
              if (err) {
                return cb(err);
              }
              user = user.toObject();
              user.token = randomToken;
              return cb(null, user);
            });
          }
        });
      });
    }
  ], function(err, data) {
    //checking for error
    if (err) {
      if (err === 'custom') {
        return req.workflow.emit('response');
      } else {
        return next(err);
      }
    }
    req.workflow.outcome.data = data;
    req.workflow.emit('response');
  }); };

Validating Sessions:

After a user successfully logs in to the app we need to handle his session, and before every API call, we have to check and match this random token which is stored in the “session_custom” table for the user and update the destroy time. If the token is a valid one and haven’t have expired yet, we can allow the user to access the application features, else the user has to log in again.

exports.validateToken = function(req, res, next) {
  req.app.utility.async.waterfall([
  /*checking whether the device id, device type and token are sent in the http request or    not*/
    function(cb) {
      if ((!req.headers[['x-auth-deviceid']]) && (!req.headers[['x-auth-devicetype']])) {
        req.workflow.outcome.errfor.device =
          req.app.utility.message.REQUIRED('deviceType and deviceId');

        return cb('custom');
      }
      if (!req.headers[['x-auth-token']]) {
        req.workflow.outcome.errfor.token =
          req.app.utility.getLang('INVALID_TOKEN', req.query.lang);
        return cb('custom');
      }
      return cb(null, 'verified');
    },
    //checking whether the given token is valid or not
    //if token is valid then update the destroy time and save it in the database
    function(msg, cb) {
      req.app.db.models.SessionCustom.findOne({
          token: req.headers[['x-auth-token']],
          deviceId: req.headers[['x-auth-deviceid']],
          deviceType: req.headers[['x-auth-devicetype']]
        })
        .populate('user')
        .exec(function(err, data) {
          if (err) {
            return next(err);
          }
          if (!data) {
            req.workflow.outcome.errfor.token =
              req.app.utility.message.INVALID('token');
            return req.workflow.emit('response');
          }
          var expiredTime = new Date(data.destroyTime);
          expiredTime.setMinutes(expiredTime.getMinutes() +
            req.app.config.User.sessionExpiredTime);
          data.destroyTime = expiredTime;
          data.save(function(err, data) {
            if (err) {
              return next(err);
            }
            return cb(null, data);
          });
        });
    }
  ], function(err, data) {
//checking for error
    if (err) {
      if (err === 'custom') {
        return req.workflow.emit('response');
      } else {
        return next(err);
      }
    }
    req.user = data.user;
    return next();
  }); 
};

Logout:

Once the user clicks on the logout button we will remove his data from the “session_custom” table and the user will be redirected back to the landing page of the app.

exports.logout = function(req, res) {
  var sendResponse = function() {
    req.workflow.outcome.message =
      req.app.utility.getLang('LOGOUT_SUCCESSFUL', req.query.lang);
    req.workflow.emit('response');
  };
  if (req.user) {
//removing the user from SessionCustom table
    req.app.db.models.SessionCustom.remove({
      user: req.user._id
    }, sendResponse);
  } else {
    sendResponse();
  } };

Signing Off!!

Hope we were able to solve the case in study. Feel free to ask any questions you have regarding this blog. See you soon with some more new stuff till then keep coding.