Want to build an app with node.js that is secured and can be trusted by the user; seems you need to handle sessions…!!
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.
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:
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.
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 –
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'); }); };
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(); }); };
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(); } };
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.