Using MongoDB and Mongoose for User Registration, Login and Logout in a Mobile Application
this mobile application tutorial shows you how to create a user registration, login and logout backend using mongodb and mongoose. this article is part of a series of mobile application development tutorials that i have been publishing on my blog jorgeramon.me, which shows you how to create a meeting room booking mobile application. this app will be used to browse an inventory of meeting rooms and reserve rooms for conference calls and other types of events. the backend that we will create in this article will connect with the user account management screens that we built in a previous chapter of this series . this backend will consist of the following modules: router (using node.js and express ) controller data model to represent a user (using mongoose ) database (using mongodb ) the router receives http requests from the mobile application and forwards them to the controller, which in turn creates, reads, updates and deletes data models defined with the mongoose library . the user profiles and sessions information will reside in a mongodb database . the router also receives data from the controller and bundles it in http responses that it sends to the mobile app. in this article we will create the controller, model and database modules using mongoose and mongodb. we will test the controller and build the router in the next article of this series. let’s proceed to install the software that we will use to create the node js, mongodb and mongoose endpoint. installing node.js node.js is a platform for building network apps that you can use to build backend endpoints for your mobile applications. you can get node.js at node.js . this tutorial doesn’t require you to have extensive knowledge of node, but you should try to learn about it as much as you can in order to take full advantage of its capabilities. to start, i would recommend the tutorials over at node school . installing express express is a framework for building web applications with node.js. express’ installation page shows you how to install the framework. in this particular article we will only use the request routing capabilities of express. in the feature we will take advantage of other features. installing mongodb mongodb is a leading nosql database at the time of this writing. in the databases ecosystem, mongodb falls under the document databases category. these are databases where each record and its associated data is thought of as a “document”. document databases have characteristics that make them a good choice for storing unstructured data across multiple servers. there is abundant online documentation on this subject. if you want to learn more, you can start with the document databases page on mongodb’s website. to install mongodb you need to head to the downloads page on mongodb.org and grab the mongodb installer for your platform. if you haven’t worked with mongo, i recommend that at a minimum you go over mongodb’s interactive tutorial so you become familiar with it. installing mongoose mongoose is a javascript library that makes it easy to move data between your application and mongodb databases. it is a layer of abstraction that allows you to create schemas for the data that your application uses, and provides facilities for connecting to mongodb and validating, saving, updating, deleting and retrieving instances of these schemas. the picture below will give you an idea of where mongoose fits in our application’s architecture: you can find installation instructions and a very good introduction to the library on mongoose’s getting started page. designing the public interface of the controller the role of the controller module in our express backend will be to fulfill requests received from the mobile application: as at this point in this series of tutorials we are only concerned with the user registration, login and logout features of the application, we will create controller methods to handle these functions. we need the controller module to respond to the following requests: register user log on a user log off a user initiate a password reset for a user finalize a password reset for a user based on these requests, we will design a controller with the following public methods. controller.register(newuser, callback): this method will register a new user with the backend by saving the user’s profile in the mongodb database. parameters: newuser – the user to register in the database callback – a function that will receive the results of the registration attempt. returns: callback – the callback function passed in the arguments. controller.logon(email, password, callback): this method will logon a user if the supplied email and password are valid. if the logon attempt succeeds, the method will add the user’s profile to a private “session” variable in the controller. parameters: email – the user’s email. password – the user’s password. callback – a function that will receive the results of the logon attempt. returns: callback – the callback function passed in the arguments. controller.logoff(): this method will log off a user by delete the user’s profile data stored in the controller’s private “session” variable. controller.resetpassword(email, callback): this method will send the user an email containing a password reset link. the link will contain a unique identifier string that will be used in the controller.resetpasswordfinal method. parameters: email – the user’s email address. callback – a function that will receive the results of the reset password attempt. returns: callback – the callback function passed in the arguments. controller.resetpasswordfinal(email, newpassword, passwordresethash, callback): this method will reset a user’s password. parameters: email – the user’s email address. newpassword – the user’s new password. passwordresethash – a unique identifier sent to the user via email from the controller.resetpassword method. callback – a function that will receive the results of the reset password attempt. returns: callback – the callback function passed in the arguments. controller.setsession(session): this method will set the controller’s private “session” variable. parameters: session – the value for the controller’s “session” variable. controller.getsession(): this method will return a reference to the controller’s private session variable. returns: session – the internal “session” variable. creating a model using mongoose as explained in the mongoose documentation , the mongoose model automatically inherits a number of methods (such as create, save, remove and find) that allow us to store and retrieve model instances from a mongodb database. we will use mongoose’s help to create a model of a user. let’s create the user.js file in the model directory. in the file, we will define the following mongoose schema: var mongoose = require('mongoose'); var schema = mongoose.schema; var userschema = new schema({ email: string, firstname: string, lastname: string, passwordhash: string, passwordsalt: string }); module.exports = mongoose.model('user', userschema); the model’s properties are the user’s attributes we want to capture (email, first name and last name), as well as a hash of the user’s password and the salt value that we used to create the password’s hash. as you will see later, storing a password’s hash and salt will allow us to authenticate users without needing to store their passwords in our database. the apiresponse class i mentioned a class called apiresponse in the majority of the methods that make the controller’s public interface. this is a data transfer class that will help us move data out of the controller. let’s create the api-response.js file in the models directory. in the file, let’s type the following definition: var apiresponse = function (cnf) { this.success = cnf.success; this.extras = cnf.extras; }; module.exports = apiresponse; any request sent to the controller will eventually produce an apiresponse instance. as its name indicates, the success property of apiresponse will signal whether the request succeeded or not. the extras property will be a javascript object containing any additional data that the controller wants to send out as part of the response. the apimessages class when the success property of the apiresponse instance is false, the data sent in the extras property can include information about what caused the failure. we will define these causes in a class that we will call apimessages. let’s create the api-messages.js file in the models directory. we will define the apimessages class as follows: var apimessages = function () { }; apimessages.prototype.email_not_found = 0; apimessages.prototype.invalid_pwd = 1; apimessages.prototype.db_error = 2; apimessages.prototype.not_found = 3; apimessages.prototype.email_already_exists = 4; apimessages.prototype.could_not_create_user = 5; apimessages.prototype.password_reset_expired = 6; apimessages.prototype.password_reset_hash_mismatch = 7; apimessages.prototype.password_reset_email_mismatch = 8; apimessages.prototype.could_not_reset_password = 9; module.exports = apimessages; as the code indicates, we are defining the reasons that can cause a controller request to fail. they are basically the different error conditions that we anticipate can occur inside the controller. creating the userprofilemodel class the data sent in the extras property of an apiresponse instance can also include a read-only version of the user’s profile. we will create the userprofilemodel class to model this entity. instances of this class will help us pass user data from the database to the outer layers of the backend, and ultimately the mobile application, without exposing sensitive information such as the password hash and salt values. in the models folder, let’s create the user-profile.js file. then, type the userprofilemodel definition: var userprofilemodel = function(cnf) { this.email = cnf.email, this.firstname = cnf.firstname, this.lastname = cnf.lastname }; module.exports = userprofilemodel; in the model we defined three properties to hold the user’s first name, last name and email. this gives us a nice data transfer object that we can send from the controller out to the mobile app when the mobile app needs to display these data. we are not storing password information in instances of this model so there is no opportunity for this information to be pulled from the database and sent out as part of an http response. creating the controller it’s finally time to turn our attention to the controller itself. let’s create the account.js file in the controllers directory. we will declare the controller as follows: var accountcontroller = function (usermodel, session, mailer) { this.crypto = require('crypto'); this.uuid = require('node-uuid'); this.apiresponse = require('../models/api-response.js'); this.apimessages = require('../models/api-messages.js'); this.userprofilemodel = require('../models/user-profile.js'); this.usermodel = usermodel; this.session = session; this.mailer = mailer; }; module.exports = accountcontroller; notice that we are injecting three dependencies into the controller. the usermodel argument is an instance of the user mongoose class that we created a few minutes ago. as you already know, this is an object that knows how to save and retrieve user data from the mondodb database. the session argument is an object that the controller will use to store session data. the mailer argument is a helper object that the controller will use to send the password reset email to the user. what we are doing here is using a dependency injection approach by passing to the controller some of the entities it needs to do its job. this will make it really easy for us to test the controller using mock objects, without having to instance the database, session and mailer objects that we will use in production. in the next chapter of this tutorial you will see how this is done when we create the tests for the controller. we are also declaring a number of variables inside the controller. the crypto and uuid variables refer to the node.crypto and node-uuid modules, which we will use to generate password hashes and unique identifiers needed when we register and log on users. the apiresponse, apimessages and userprofile internal variables refer to the model classes with the same names that we created a few minutes ago. the session getter and setter methods let’s move on to implementing the controller’s public interface that we designed earlier. first, we will create the setter and getter methods for the session, immediately below the controller’s declaration: accountcontroller.prototype.getsession = function () { return this.session; }; accountcontroller.prototype.setsession = function (session) { this.session = session; }; we will use these methods to set or grab a reference to the controller’s session variable. the hashpassword method we will use this method to create a cryptographically-strong pseudo random hash of a password: accountcontroller.prototype.hashpassword = function (password, salt, callback) { // we use pbkdf2 to hash and iterate 10k times by default var iterations = 10000, keylen = 64; // 64 bit. this.crypto.pbkdf2(password, salt, iterations, keylen, callback); }; within hashpassword, we call crypto.pbkdf2, which uses a pseudorandom function to derive a key of the given length from the given password, salt and number of iterations. remember that we will save this hash in the database, instead of saving the password in clear text or encrypted. this is a good security measure because it’s very difficult to use the hash to obtain the original password without knowing the function used, salt, iteration and keylen values. the logon method next, we will create the logon method: accountcontroller.prototype.logon = function(email, password, callback) { var me = this; me.usermodel.findone({ email: email }, function (err, user) { if (err) { return callback(err, new me.apiresponse({ success: false, extras: { msg: me.apimessages.db_error } })); } if (user) { me.hashpassword(password, user.passwordsalt, function (err, passwordhash) { if (passwordhash == user.passwordhash) { var userprofilemodel = new me.userprofilemodel({ email: user.email, firstname: user.firstname, lastname: user.lastname }); me.session.userprofilemodel = userprofilemodel; return callback(err, new me.apiresponse({ success: true, extras: { userprofilemodel:userprofilemodel } })); } else { return callback(err, new me.apiresponse({ success: false, extras: { msg: me.apimessages.invalid_pwd } })); } }); } else { return callback(err, new me.apiresponse({ success: false, extras: { msg: me.apimessages.email_not_found } })); } }); }; inside logon we first create the me variable to hold a reference to the accountcontroller instance that we can use inside callback functions that we will create inline. next, we call the findone method of the usermodel instance to try to find a user with the same email in the mongodb database. the findmethod is provided by mongoose. remember that usermodel is an instance of the user model that we create with mongoose’s help. if the call to findone produces an error, we immediately invoke the callback argument, passing an apiresponse instance where the success property is set to false and the extra property contains a message that explains that there was a database error. if the call to findone produces a user, we proceed to hash the password provided by the user who is attempting to log on, and compare the hash to the password hash of the user that we found in the database. if the hashes are equal, it means that the user attempting to log on provided a valid password and we can move on to create a userprofile instance and save it to the controller’s session variable. we then invoke the callback function, setting the response’s success property to true and passing the userprofile instance in the extras property of the response. when the hashes don’t match, we invoke the callback function, setting the response’s success property to false and passing an “invalid password” reason in the extras property. finally, if the call to findone does not produce a user, we invoke the callback function with a response where the extras property contains a message indicating that the provided email was not found. the logoff method we will use the logoff method to terminate a user’s session: accountcontroller.prototype.logoff = function () { if (this.session.userprofilemodel) delete this.session.userprofilemodel; return; }; to terminate the session we simply destroy the userprofile instance that we previously saved in the controller’s session variable. the register method the controller’s register method allows a user to register with the application: accountcontroller.prototype.register = function (newuser, callback) { var me = this; me.usermodel.findone({ email: newuser.email }, function (err, user) { if (err) { return callback(err, new me.apiresponse({ success: false, extras: { msg: me.apimessages.db_error } })); } if (user) { return callback(err, new me.apiresponse({ success: false, extras: { msg: me.apimessages.email_already_exists } })); } else { newuser.save(function (err, user, numberaffected) { if (err) { return callback(err, new me.apiresponse({ success: false, extras: { msg: me.apimessages.db_error } })); } if (numberaffected === 1) { var userprofilemodel = new me.userprofilemodel({ email: user.email, firstname: user.firstname, lastname: user.lastname }); return callback(err, new me.apiresponse({ success: true, extras: { userprofilemodel: userprofilemodel } })); } else { return callback(err, new me.apiresponse({ success: false, extras: { msg: me.apimessages.could_not_create_user } })); } }); } }); }; the first step that we take in register is to check if a user with the same email address of the user that is attempting to register exists in the database. as we did in the logon method, if there is a database error we will immediately invoke the callback function and send out an apiresponse instance explaining that there was a database error. if we find an user that has the same email address of the user that is attempting to register, we also stop the registration process, as we cannot have two users with the same email address. in this case the extras property of the apiresponse instance that we send out contains a message explaining that the email address already exists. if we don’t find the email address in the database, we proceed to save the new user by invoking save method (inherited from mongooose) of the user class. the save method produces a numberaffected argument in its callback function. we check numberaffected to make sure that the new user was saved. if numberaffected is 1, we create a userprofile instance and send it out embedded in an apiresponse object. if numberaffected is not 1, we produce an apiresponse indicating that the registration failed. the resetpassword method the resetpassword method is the first step of the password reset workflow that we defined in the mobile application user registration, login and logout screens tutorial of this series. the method consists of the following code: accountcontroller.prototype.resetpassword = function (email, callback) { var me = this; me.usermodel.findone({ email: email }, function (err, user) { if (err) { return callback(err, new me.apiresponse({ success: false, extras: { msg: me.apimessages.db_error } })); } // save the user's email and a password reset hash in session. we will use var passwordresethash = me.uuid.v4(); me.session.passwordresethash = passwordresethash; me.session.emailwhorequestedpasswordreset = email; me.mailer.sendpasswordresethash(email, passwordresethash); return callback(err, new me.apiresponse({ success: true, extras: { passwordresethash: passwordresethash } })); }) }; in order to initiate a password reset sequence, users need to provide their email address. inside resetpassword we use the provided email address to retrieve the user’s record from the database. if the record exists, we create a unique identifier called passwordresethash, and pass this identifier and the user’s email address to the mailer object’s sendpasswordresethash method. this method sends a message to the user, containing the unique identifier and a password reset link that they can use to change their password. we will implement the mailer module in the next chapter of this tutorial. inside resetpassword we also save the password reset hash and the user’s email in the controller’s session variable so we can later compare them to the values provided by the user in the final step of the password reset process. if the database doesn’t have a record for the provided email address, we return an apiresponse whose extras property explains that the email was not found. the resetpasswordfinal method users will invoke this method when they access a special web page using the “password reset” link inside the email that they will receive after they perform the first step of the password reset process. here’s the code for the method: accountcontroller.prototype.resetpasswordfinal = function (email, newpassword, passwordresethash, callback) { var me = this; if (!me.session || !me.session.passwordresethash) { return callback(null, new me.apiresponse({ success: false, extras: { msg: me.apimessages.password_reset_expired } })); } if (me.session.passwordresethash !== passwordresethash) { return callback(null, new me.apiresponse({ success: false, extras: { msg: me.apimessages.password_reset_hash_mismatch } })); } if (me.session.emailwhorequestedpasswordreset !== email) { return callback(null, new me.apiresponse({ success: false, extras: { msg: me.apimessages.password_reset_email_mismatch } })); } var passwordsalt = this.uuid.v4(); me.hashpassword(newpassword, passwordsalt, function (err, passwordhash) { me.usermodel.update({ email: email }, { passwordhash: passwordhash, passwordsalt: passwordsalt }, function (err, numberaffected, raw) { if (err) { return callback(err, new me.apiresponse({ success: false, extras: { msg: me.apimessages.db_error } })); } if (numberaffected < 1) { return callback(err, new me.apiresponse({ success: false, extras: { msg: me.apimessages.could_not_reset_password } })); } else { return callback(err, new me.apiresponse({ success: true, extras: null })); } }); }); }; to reset their password a user will need to provide their email address and a new password, along with the password reset hash that we sent them in the password reset email generated from the resetpassword method. we will save the user from having to type the password reset hash by embedding the hash in the link inside the password reset email. in the next chapter of this series we will create the mailer class and implement the email features. inside resetpasswordfinal, we first check that the password reset hash is also saved in the controller’s session variable. if the hash does not exist, we return an apiresponse whose extras property explains that the password reset period expired. as a security measure, we want to limit the period of time during which a user can reset their password to the length of a session timeout period. if the password reset hash value stored in the session and the value supplied by the user do not match, we will assume that the user who requested the password reset and the user who is providing the new password are not the same. in such a case we return an apiresponse explaining that there is a mismatch of the hashes. the same logic applies when the email value stored in the session and the value supplied by the user do not match, in which case we return an apiresponse explaining that there is a mismatch of the email addresses. if the password reset hash and email address validations are successful, we proceed to hash the new password and save it by calling the user model’s update method, which is inherited from mongoose. the update method returns the number of records affected by the update operation. we check this value and return an apiresponse that signals to the outside world if the update operation succeeded or not. summary and next steps we just began building the backend for a meeting room booking application that we defined in the first chapter of this series . this is a mongodb and mongoose backend paired to a node.js and express web server. our focus in this article was building a controller module that will handle the user registration, login and logout features of the application. we implemented the controller’s public interface, along with a number of helper classes that will allow the controller to do its work. in the next chapter of this tutorial we will turn our attention to testing the controller, which will take us through choosing a testing library and implementing the tests for the controller’s features. make sure to sign up for miamicoder’s newsletter so you can be among the first to know when next part of this tutorial is available. download the source code download the mongodb and mongoose backend tutorial here: mongodb and mongoose backend for mobile application previous chapters of this series these are the previous parts of this series: mobile app tutorial: the meeting room booking app, part 1 mobile app tutorial: the meeting room booking app, part 2 mobile app tutorial: the meeting room booking app, part 3 mobile ui patterns – a flowchart for user registration, login and logout
December 17, 2014
by Jorge Ramon
·
93,546 Views
·
2 Likes