Building a Login System in Node.js and MongoDB

This past week I finally got around to playing with Node.js and am really impressed with how simple it was to get a server and database up and running in literally minutes.

Once there, I thought a good first project to explore the platform would be to try building a simple login system analogous to what I feel like I’ve built a million times in mysql & php.

So after thinking about it a bit, I knew these were the basic features I wanted to implement :

Login Panel

  • Just a simple login panel with the ability to reset a lost password and of course the ability to create an account for first time users.
  • An option to “remember me” that saves the user’s data in a local cookie allowing them to bypass the login screen whenever they revisit the site.


New Account Screen

  • A simple form that allows a new user to set their username & password,
    as well as some personal data such as their real name, email and location.

Account Update Screen

  • Almost identical to the account creation screen, with the slight modification that the username field is deactivated so it cannot be changed allowing us to preserve a unique key to identify each account.
  • Here we’ll also add a button that will allow user’s to delete their account.

Password Retrieval

  • A simple modal window where users can request an email to reset their password.

If you’d like to jump ahead :

Standing on the Shoulders of Giants

One of the most impressive highlights of the Node.js ecosystem in my opinion is the incredible myriad of libraries actively being produced to take much of the heavy lifting out of building your app.

In our login system we’ll leverage the following libraries to get us up and running :

  • Express.js – A Node.js framework with a ton of convenient features that make working in Node much faster
  • MongoDb – A NoSQL database we’ll use to save our account data
  • Jade – A templating engine that allows us to write less verbose HTML
  • Stylus – A CSS-preprocessor with a zillion amazing features that greatly take the pain out of writing traditional CSS
  • Email.js – Middleware to easily dispatch emails from our Node.js server
  • Moment.js – A lightweight library for convenient date parsing & formatting

And last but not least the inimitableTwitter Bootstrap UI library to layout our forms and pages with beauty and consistency across browsers.

Application Structure

Our login system will of course need to execute code in two environments,
on the client machine and on the server.

On the client side we’ll need to display our HTML pages, handle user interactions, and validate the various forms our app uses before sending their data to the server.

On the server side we’ll layout our HTML pages using Jade templates and create a few custom modules to read and write to the database and dispatch emails for password retrieval.

The general layout of these two environments is as follows :

Server-Side Components :

  • views – jade templates that compile to HTML
    • login.jade
    • home.jade
    • signup.jade
  • modules – helper classes that interact with the database and dispatch emails
    • account-manager.js
    • email-dispatcher.js

Client-Side Components :

  • views – these setup our form controllers & modal windows
    • login.js
    • home.js
    • signup.js
  • controllers – handle user interactions
    • loginController.js
    • homeController.js
    • signupController.js
  • form-validators – validate forms and display errors
    • loginValidator.js
    • accountValidator.js
    • resetValidator.js
    • emailValidator.js

Note : Because the new account and update account forms are so similar, I’ve consolidated the code that validates them into one file called AccountValidator and then put any code that differs between them in their respective controllers SignupController & HomeController.

So How Does All This Actually Work?

The basic page flow can be generalized into two parts:

Part 1 : Getting the Page

  1. A user arrives at http://node-login.braitsch.io/ and requests the root page or “/”
  2. Router.js on our server sees this GET request and returns ‘login.jade’, the view associated with http://node-login.braitsch.io/

However before it does this, it checks the GET request object for a username & password cookie and if they exist and validate, redirects the browser to http://node-login.braitsch.io/home

var AM = require('./modules/account-manager');
app.get('/', function(req, res){
// check if the user's credentials are saved in a cookie //
   if (req.cookies.user == undefined || req.cookies.pass == undefined){
      res.render('login', 
         { locals: 
            { title: 'Hello - Please Login To Your Account' }
         }
      );
   } else{
   // attempt automatic login //
      AM.autoLogin(req.cookies.user, req.cookies.pass, function(o){
         if (o != null){
            req.session.user = o;
            res.redirect('/home');
         }  else{
            res.render('login', 
               { locals: 
                  { title: 'Hello - Please Login To Your Account' }
               }
            );
         }
      });
   }
});
  1. Otherwise, the server renders login.jade into the HTML login form and sends it to the browser.
  2. Once the HTML is received by the client, the script tags in the page request the JavaScript files associated with the login page, namely :
    • /js/views/login.js
    • /js/controllers/loginController.js
    • /js/form-validators/loginValidator.js
    • /js/form-validators/emailValidator.js
  3. These four component files setup the form and alert windows, listen for user interaction and validate the form before sending it back to the server.

Part 2 : Posting the Page

  1. A user enters their username & password and hits “submit”
  2. loginValidator.js validates the form and then allows login.js to send its contents to the server as a POST request.
  3. Router.js on the server sees the incoming POST request and forwards the username & password to the AccountManager module which compares what the user entered to the values stored in the database. Once Router.js gets a response from the AccountManager it either sends a 200 (pass) or 400 (fail) status code back to the browser.

var AM = require('./modules/account-manager');
var EM = require('./modules/email-dispatcher');
app.post('/', function(req, res){
   if (req.param('email') != null){
      AM.getEmail(req.param('email'), function(o){
         if (o){
	    res.send('ok', 200);
	    EM.send(o, function(e, m){ console.log('error : '+e, 'msg : '+m)});	
	 } else{
	    res.send('email-not-found', 400);
	 }
      });
   } else{
  // attempt manual login //
   AM.manualLogin(req.param('user'), req.param('pass'), function(e, o){
      if (!o){
         res.send(e, 400);
      }	else{
	 req.session.user = o;
      if (req.param('remember-me') == 'true'){
	 res.cookie('user', o.user, { maxAge: 900000 });
	 res.cookie('pass', o.pass, { maxAge: 900000 });
      }			
	 res.send(o, 200);
      }
    });
   }
});
  1. login.js which owns the login form, hears the returned value and either redirects the user to the logged in page, or shows an alert window that displays a specific error message.

var lv = new LoginValidator();
var lc = new LoginController();
 
// main login form //
 
$('#login-form').ajaxForm({
   beforeSubmit : function(formData, jqForm, options){
      if (lv.validateForm() == false){
         return false;
      } else{
// append 'remember-me' option to formData to write local cookie //					
	 formData.push({name:'remember-me', value:$("input:checkbox:checked").length == 1})
	 return true;
      }
   },
   success : function(responseText, status, xhr, $form){
      if (status == 'success') window.location.href = '/home';
   },
   error : function(e){
      lv.showLoginError('Login Failure', 'Please check your username and/or password');
   }
});

This communication sequence between the server and the client is essentially what is happening on each page of our app.

In Summary

  1. The user arrives at a page
  2. Router.js returns the appropriate Jade template that renders the page’s HTML and loads its JavaScript controllers
  3. The user interacts with the page typically by filling out and submitting a form
  4. Some JavaScript validates the form and sends its data back to the server
  5. Router.js forwards the data to the AccountManager for comparison, entry, deletion, etc. in the database
  6. The AccountManger returns a pass or fail response back to Router.js which then gets sent back to the client
  7. JavaScript on the client handles the response by either redirecting the browser or showing a modal window with a detailed response.

As you can see the communication between the client and server is not terribly complicated however it does beg the question as to how to best organize this communication into components and modules.

So with that in mind, the source for this app is fully available on github with instructions on how to install and get this running on your local machine.

As always feedback, questions and suggestions for improvement are most welcome.

  • braitsch

    It shouldn’t require much to migrate the app to Express3, it’s on my to do list but I haven’t had the time. If you are able to successfully migrate the project, go ahead and issue a pull request.

  • braitsch

    This is on my list to do. If anyone migrates the project to Express3 before I get to it go ahead and issue a pull request.

  • braitsch

    I just updated the app to Express.js v3.0.6 as of v1.3.0.

  • Florent Wozniak

    Many thinks Braitsch i really appreciate. I guess you now use ‘extends’ instead of layout in .jade file (got it) :) did you make other changes ?

  • Guest

    Another security issue: the password should absolutely not be saved in a cookie on the user’s machine. (Also, it’s not marked httpOnly, which would have helped a little, but not enough.)

  • bren101

    Two small suggestions:

    1. Upgrade to bcrypt for more robust salt & hash quality, and less lines of code as a bonus.

    2. Write the time/date to mongodb as a native Date() object, so that it translates into a native mongodb ISODate structure.

  • http://twitter.com/snusmubrik Andrew Eremenko

    Thank you for this great app. But I have some questions. Why did you store login and password in cookies? It’s not secure, isn’t it?

    How about idea to make auth token (or whatever) like at twitter or facebook?

  • http://twitter.com/EricFleischmann Eric Fleischmann

    This is righteous. Thanks for sharing. Will try to get it to work with mongoose and bcrypt for my application.

  • Mark Barton

    Hi,

    Great example – thanks.

    One thing – on the updatePassword method in the account-manager.js file, I couldnt get it to return a HTTP 200 unless I amended the callback which was within the account.save call i.e.

    Originally it was this:

    accounts.save(o, {safe: false}, callback);

    I added the account object so something was returned back to the callng method.

    accounts.save(o, {safe: false}, callback(o));

    I am new to node so I wasnt sure if this was correct – else I would have done the update via GIT.

    I am going to have a go at using mongoose as a learning exercise – do you foresee any issues?

    Thanks

    Mark

  • Etna Mianuloe

    Thanks for tutorial!! Trying to figure it out.. Hard as I am a beginner. In Part 2 point 2 loginValidator.js validates the form and then allows login.js to send its contents to the server as a POST request. What’s the code for it in login.js?

  • Alex

    Thanks for this practical example. I was testing the app out on localhost and tried a password rest.

    I could not reset my password successfully with the message “I’m sorry something went wrong, please try again.”

    Assuming i have configured my smtp server setting correctly, what could be wrong here?

    On the node app.js screen:
    TypeError: Cannot read property ‘email’ of undefined ..blah blah

    I noticed your website did not have the smtp notification setup too.

    Many thanks in advance. :)

  • http://www.facebook.com/profile.php?id=600897893 John F Dutcher

    It’s such a nice sample. But oddly..if I install mongodb with npm…a mongodb folder is created in node_modules that has no bin folder and no mongodb.exe or mongo.exe with which to start the server. If I replace the mongodb folder in node_modules with an empty one and unzip the windows download from mongodb web site into it…all needed exe’s are there and I can start the server db and the shell. BUT the node-login javascript scripts in the ‘sever’ folder of the app fail when trying to ‘require’ mongodb ( require (‘mongodb’).db ) I don’t get it.

  • http://www.facebook.com/hugh.ci Hugh Cirr

    Yes i straight away changed it to sessions. Very simple to do

  • jane luo

    Hello!

    I am not understanding how router.js works. I also also don’t understand how a success is generated. I’m kind of a newb at this so any feedback would be amazing! PS. How do I display the website on localhost? Thank you! :)

  • Alexander Morales

    Hi Stephen, I only have a little question.
    When I try to access to /print section, this cannot validate if user is logged-in and automatically enter to this page and everyone can access to the list of Users. You have any idea for change that.

    Thankyou in advance.

  • Julien

    Great Article :)

  • MrCatt

    Nice!
    Would you care giving some pointers on how to remove mongodb and replace it with mysql?

  • braitsch

    Sure just add “if (req.session.user == null) res.redirect(‘/’);” to the beginning of the app.get(‘/print’) function body in app/server/router.js. This will redirect the request back to the login page if the user is not logged in. Take a look at app.get(‘/home’) as an example.

  • http://www.elblogdeklank.net/ Alexander Morales

    Ok, really work for me, thankyou so much, really help me!

  • bren101

    NPM just installs a node package which is a “driver” for mongodb.. an interface between your app and a proper install of mongodb. It doesn’t actually include the mongodb application, which needs to be installed outside of NPM.

  • Gary Tse

    I installed MongoDB and Node-Login on a VPS running CentOS and tried to start the app using “node app”. I then get:
    Express server listening on port 8080
    connected to database :: node-login

    And it just hangs there (I can’t enter commands anymore in my shell).

    I’m new to all this, is there something I’m missing?

  • ppasindu

    I brached node login for Mysql and any other db, done using jugglingdb , use any juggling adapter to work with .

    https://github.com/pasindud/node-login

  • praveen mohanty

    modals not working in IE 10. Can any one please help!!!

  • Ray

    Are the passwords encrypted in this script? And if they are, what encryption algorithm are you using?

  • fs21

    thanks for your trips, hah

  • lumpawire

    Your code has a big security hole. It allows password change for any email id. Auth code is ignored when stripped. However it’s a good start for beginners though.

  • Jorge Castillo

    How do you change to sessions ???