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.

  • flo

    Tank’s for sharing such great dev. Can somebody tell ma what print.jade and account.jade use for ?

  • hugozap

    Why is app.use(exp.static(app.root + ‘/app/server’)); required? Doesn’t it expose the server folder contents?

    • braitsch

      You’re right that does and was a security vulnerability. It was there to allow access to vendor libs that were in /server/vendor. Those files have since been moved to /public/vendor as of v1.2.1

  • bren101

    For security, shouldn’t the validation code really be running on the server, not the client?

    • braitsch

      Authentication is happening on the server, what are you talking about?

      • bren101

        I’m talking about all the code in form-validators. Apologies if I’m reading it wrong, but if a malicious user bypasses the client-side validation, doesn’t it mean the server might do things such as write invalid email addresses into its database?

        • braitsch

          Ah yes I see what you’re talking about. Sure the client side validation just attempts to protect against common user errors such as an invalid email or empty form field. If you wanted to protect against something say like a SQL injection you’d want to add that sanitization in the AccountManager before allowing a write to the DB.

      • Corey Cauble

        I think he is talking about the validation of the input. It should be scrubbed by the server before processing it, to make sure they are passing valid input not scripting etc.

  • tylerh

    This is sweet thanks for posting this!

  • cdscscs

    Hi Stephen, I’m new to Node and I’d like to ask you what are the
    modifications that need to be done to port your code to Express 3 ? I
    struggle for the last two day with it wo success :-( I’ve made lots of
    search in google and also asked in Stakeoverflow forum but with no
    response. Express and firebug return to me no error. It is very
    frustrating because your code works nicely with Express 2.

    Thank you for your reply (even a clue would be great).

    Flo

    • 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

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

        • cdscscs

          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 ?

  • braitsch

    Updated to express.js v3.0.6 as of v1.3.0

  • 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?

    • Hugh Cirr

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

      • Jorge Castillo

        How do you change to sessions ???

  • 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.

  • markbarton

    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.

    • 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.

  • ppasindu

    Great Work, This is my Mysql Branch using jugglingdb ORM and jugglingdb-mysql adapter

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

  • 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! :)

  • http://www.elblogdeklank.net/ 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.

    • 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!

  • Julien

    Great Article :)

  • http://postimg.org/image/eiv5k4n3n/full/ MrCatt

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

  • 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?

    • TNLIN

      u need to open ur browser,enter “localhost:8080″

    • Paul

      The command line blocks because the server is waiting for http requests, if you want to get your command line back and have the server keep running put an ‘&’ at the of the command, like so: node app.js &

  • 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.

  • manish

    on mac , i ran mongodb and then called node app . mongodb is runnign and so is this saying $ node app

    Express server listening on port 8080

    connected to database :: node-login

    how ever the webpage http://localhost:8080/ says no data received

  • TNLIN

    buz you need mongoDB

  • Enrique Romero

    Newbie here. Anyone know how to get rid of the “Fork me on git hub” message appearing on the .jade files? Tks

  • Vaibhav Lokesh

    The email feature is not working, how to set up the mailer code so as when the users forgets his password an automatic random generated password is sent to his/her mail, please reply asap…!

    • Paul

      you need to change 2 things. 1 – the email-settings.js file in the sevver/modules directory and 2 – the return link in the composeEmail method of the email-dispatcher.js in the server/modules. That should do it.

  • Guest

    Hi !
    What shall I do if I want to expand this page and add more .jade templates , redirections etc.

    I have made very similar to print.jade file and added reference to it in router.js in the same way it is done with ‘/print’ but it doesn’t work :( .

    Thanks in advance. I am rookie to node.js .

  • buffalo

    how is it done that print.jade sees accts object where is it injected into it ?

  • Andrew Nyhus

    How do I connect this to my own mongolab database? I see how to change the port number in dbPort in the account-manager.js file but I can’t figure out how to change it to my own database. Thanks in advance!

  • Shanker Sai

    Wonderful tutorial for node.js login………thank you so much……