One of the trickiest aspects of building my first application was implementing User Authentication. The hours I spent with my head against the keyboard trying to will it to work, instead of gleefully logging in and out, will never be regained. So, let me help you navigate these tricky waters! In this series we are going to build a simple application in order to explore implementing user authentication off of MongoDB with a Node application running an Express server combined with Passport. We will be building local authentication, as well as authenticating through Twitter, Google, and Facebook. Each installment of this series I will show how to implement a new User Authentication Strategy, as Passport calls them, addressing each of our four methods mentioned. This first post covers setting up our application and the joys of locally authenticating users!

Getting Started

To start, let's go over the technologies we will be using:

In this post we will:

  • Set up a virtual server running MongoDB
  • Build our basic application
  • Create our template views
  • Write routes for our Local functionality
  • Configure Passport for Local login and signup
  • Test our Local login and signup
  • Pat ourselves on the back and tell our friends how cool we are!

Deploy a New Virtual Server with MongoDB

If you don’t have a CenturyLink Cloud account yet, head over to our website and sign up for a free trial. You’ll need it to access CenturyLink Cloud products.

Our first step is to deploy a new CenturyLink Cloud virtual server. Follow the steps below.

  1. Log into the CenturyLink Cloud control portal at https://control.ctl.io/
  2. On the left side menu, click Infrastructure and then click Servers.

    Auth1

  3. On the left-hand side of the server panel, click on the region for the server we will provision.

  4. Click create and then server.
  5. Complete the setup form for your new server. Be sure to fill out the fields for server name and admin/root password.
  6. For operating system, select "CentOS 7 | 64-bit".
  7. Click create server.
  8. Your server provisioning request will enter the queue. You can watch the progress of your request on the screen. Your server is provisioned when the status of all tasks in the queue is complete.

    CLC Auth

  9. After your new server is provisioned, in the CenturyLink control portal, click Infrastructure on the left side menu, and then click Servers.

  10. Navigate to your new server and click on its name.
  11. Click the more menu, and then click add public ip.
  12. Check the box for SSH/SFTP (22).
  13. Click custom port... and then single port.
  14. Type "27017" in the blank box to open up the MongoDB server port.

    Passport authentication strategy

  15. Click add public ip address.

Installing and Configuring MongoDB

  1. Navigate to your server in the CenturyLink Cloud control panel as in the previous section. Your server's public IP address will be noted on the screen.
  2. From a shell on your local machine, connect to your new server with the following command. Replace "YOUR.VPS.IP" with your server's public IP address.

    ssh [email protected]
    
  3. Install the MongoDB server software by running the following commands.

    $ yum install -y mongodb mongodb-server
    
  4. With your favorite text editor, open /etc/mongod.conf. Look for the line that begins "bind_ip" and comment it out. The top of your file should now look like this:

    ##
    ### Basic Defaults
    ##
    
    # Comma separated list of ip addresses to listen on (all local ips by default)
    #bind_ip = 127.0.0.1
    
  5. Start the MongoDB service by running the following command.

    $ service mongod start
    

Basic Application

First up, we need our basic application. If you already have Node.js and npm set up on your computer, then by all means carry on! If you do not, drop by the Node.js website to set yourself up and we will be waiting for you here. Now then, let's go ahead and establish our dependencies and install them. You will need to either build your package.json file or you may clone the code from the GitHub repository linked at the end of this post. In your package.json make sure you have at least the following:

{
  "name": "userAuth",
  "main": "index.js",
  "dependencies": {
    "bcryptjs": "^0.7.12",
    "express": "^3.5.0",
    "express-handlebars": "^3.0.0",
    "mongodb": "^2.2.11",
    "passport": "^0.2.0",
    "passport-facebook": "^1.0.3",
    "passport-google": "^0.3.0",
    "passport-local": "^1.0.0",
    "passport-twitter": "^1.0.2",
    "q": "^1.0.1"
  }
}

After making sure you list the above dependencies in your package.json go ahead and run:

$ npm install

It is time to build our server now that we have all of our dependencies. (Note: not all of these dependencies will be used in this post. That is alright since we will need them in the future) Let's move over to our index.js file, which will hold our server, and bring in our necessary dependencies:

//index.js/
var express = require('express'),
    exphbs = require('express-handlebars'),
    logger = require('morgan'),
    cookieParser = require('cookie-parser'),
    bodyParser = require('body-parser'),
    methodOverride = require('method-override'),
    session = require('express-session'),
    passport = require('passport'),
    LocalStrategy = require('passport-local'),
    TwitterStrategy = require('passport-twitter'),
    GoogleStrategy = require('passport-google'),
    FacebookStrategy = require('passport-facebook');

//We will be creating these two files shortly
// var config = require('./config.js'), //config file contains all tokens and other private info
//    funct = require('./functions.js'); //funct file contains our helper functions for our Passport and database work

var app = express();

//===============PASSPORT===============

//This section will contain our work with Passport

//===============EXPRESS================
// Configure Express
app.use(logger('combined'));
app.use(cookieParser());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.use(methodOverride('X-HTTP-Method-Override'));
app.use(session({secret: 'supernova', saveUninitialized: true, resave: true}));
app.use(passport.initialize());
app.use(passport.session());

// Session-persisted message middleware
app.use(function(req, res, next){
  var err = req.session.error,
      msg = req.session.notice,
      success = req.session.success;

  delete req.session.error;
  delete req.session.success;
  delete req.session.notice;

  if (err) res.locals.error = err;
  if (msg) res.locals.notice = msg;
  if (success) res.locals.success = success;

  next();
});

// Configure express to use handlebars templates
var hbs = exphbs.create({
    defaultLayout: 'main', //we will be creating this layout shortly
});
app.engine('handlebars', hbs.engine);
app.set('view engine', 'handlebars');

//===============ROUTES===============

//This section will hold our Routes

//===============PORT=================
var port = process.env.PORT || 5000; //select your port or let it pull from your .env file
app.listen(port);
console.log("listening on " + port + "!");

Now that we have built our server we can test it:

    $ node index.js

If our application is functioning properly we should see a message, part of which reads, "listening on port 5000!" or whichever port you indicated your application to run on. Go ahead and kill the server by pressing Control-C, it is time to add our templates so that we will have something pretty to look at!

If you have not cloned the repo, create a "views" folder in your project's directory. Within that folder we will have two files (home.handlebars and signin.handlebars) and one folder (layouts). The layouts folder will contain one file (main.handlebars) which is the default layout we chose when we configured our Express application. Our main.handlebars file will look like the following:

<!-- views/layouts/main.handlebars -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="description" content="User Authentication">
  <meta name="author" content="">

  <title>User Authentication</title>

  <!-- Latest compiled and minified CSS -->
  <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css">

  </head>

  <body>

    <div class="container">

      <nav class="navbar navbar-default" role="navigation">
      <div class="container-fluid">

      <!-- Brand and toggle get grouped for better mobile display -->
      <div class="navbar-header">
        <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
          <span class="sr-only">Toggle navigation</span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
        </button>
      </div>

        <!-- Collect the nav links, forms, and other content for toggling -->
        <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
          <ul class="nav navbar-nav">
            <li>
              {{#if user}}
                <p class="navbar-text">
                  <strong>Hi,</strong>
                  <img src="{{user.avatar}}" width="20" height="20">
                  {{user.username}}
                </p>
                </li>
                </ul>
                <ul class="nav navbar-nav navbar-right">
                  <li>
                    <a href="/logout">Log Out</a>
                  </li>
              {{else}}
                <a href="/signin">Sign In</a>
                </li>
              {{/if}}
          </ul>
        </div><!-- /.navbar-collapse -->
      </div><!-- /.container-fluid -->
    </nav>

    {{#if error}}
      <p class="alert alert-warning">{{error}}</p>
    {{/if}}

    {{#if success}}
      <p class="alert alert-success">{{success}}</p>
    {{/if}}

    {{#if notice}}
      <p class="alert alert-info">{{notice}}</p>
    {{/if}}

    <!--where our other templates will insert-->
    {{{body}}}

    </div> <!-- /container -->

    <!-- Bootstrap core JavaScript
    ================================================== -->
    <!-- Placed at the end of the document so the pages load faster -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>

    <!-- Latest compiled and minified JavaScript -->
  <script src="//netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js"></script>
</body>
</html>

Our home.handlebars file should look like the following for now:

<!-- views/home.handlebars -->
{{#if user}}
  <div class="jumbotron">
    <h1>MongoDB User Authentication</h1>
    <p>Below is your profile information!</p>
  </div>
  <div class="panel panel-default">
    <div class="panel-heading">
      <h3 class="panel-title">Profile Information</h3>
    </div>
    <div class="panel-body">
      <p>Username: {{user.username}}</p>
      <p>Avatar: <img src="{{user.avatar}}"/></p>
    </div>
  </div>

{{else}}
  <div class="jumbotron">
    <h1>MongoDB User Authentication</h1>
    <p>Sign in and view your profile!</p>
    <p>
      <a href="/signin" class="btn btn-primary btn-lg" role="button">
        <span class="glyphicon glyphicon-user"></span>
        Sign in!
      </a>
    </p>
  </div>

{{/if}}

Finally, our signin.handlebars file should look like the following:

<!-- views/signin.handlebars -->
<div class="jumbotron">
  <h1>Sign in</h1>
  <p>We're using passport.js to demonstrate user authentication. Please sign-in with your Local, Twitter, Google, or Facebook account to view your profile.</p>
  <p>
    <a data-toggle="collapse" href="#local" class="btn btn-warning btn-lg" role="button"><span class="glyphicon glyphicon-user"></span> Sign in Locally</a>
    <a href="/auth/twitter" class="btn btn-info btn-lg" role="button"><span class="glyphicon glyphicon-user"></span> Sign in with Twitter</a>
    <a href="/auth/google" class="btn btn-danger btn-lg" role="button"><span class="glyphicon glyphicon-user"></span> Sign in with Google</a>
    <a href="/auth/facebook" class="btn btn-primary btn-lg" role="button"><span class="glyphicon glyphicon-user"></span> Sign in with Facebook</a>
  </p>

  <div id="local" class="collapse">
    <a data-toggle="collapse" href="#local-sign-in" class="btn btn-default btn-md" role="button">I already have an account</a>
    <a data-toggle="collapse" href="#local-reg" class="btn btn-default btn-md" role="button">I need to make an account</a>
  </div>

  <form id="local-sign-in" class="collapse" action="/login" method="post">
    <div>
        <p></p>
        <label>Username:</label>
        <input type="text" name="username"/>
    </div>
    <div>
        <label>Password:</label>
        <input type="password" name="password"/>
    </div>
    <div>
        <input type="submit" class="btn btn-primary btn-sm" value="Log In"/>
    </div>
  </form>

  <form id="local-reg" class="collapse" action="/local-reg" method="post">
    <div>
        <p></p>
        <label>New Username:</label>
        <input type="text" name="username"/>
    </div>
    <div>
        <label>New Password:</label>
        <input type="password" name="password"/>
    </div>
    <div>
        <input type="submit" class="btn btn-primary btn-sm" value="Register"/>
    </div>
  </form>
</div>

Routes

Now that we have our layouts in place, let's write some routes so that we can see them! In the routes section of our index.js file add the following code to get us up and running:

//===============ROUTES=================
//displays our homepage
app.get('/', function(req, res){
  res.render('home', {user: req.user});
});

//displays our signup page
app.get('/signin', function(req, res){
  res.render('signin');
});

//sends the request through our local signup strategy, and if successful takes user to homepage, otherwise returns then to signin page
app.post('/local-reg', passport.authenticate('local-signup', {
  successRedirect: '/',
  failureRedirect: '/signin'
  })
);

//sends the request through our local login/signin strategy, and if successful takes user to homepage, otherwise returns then to signin page
app.post('/login', passport.authenticate('local-signin', {
  successRedirect: '/',
  failureRedirect: '/signin'
  })
);

//logs user out of site, deleting them from the session, and returns to homepage
app.get('/logout', function(req, res){
  var name = req.user.username;
  console.log("LOGGIN OUT " + req.user.username)
  req.logout();
  res.redirect('/');
  req.session.notice = "You have successfully been logged out " + name + "!";
});

Even though we have not set up our Passport Strategies yet, we can now run our application and view the homepage and sign in page from our browsers. We are well on our way!

Passport

Speaking of those Passport Strategies, let's take a moment to discuss how Passport works. According to the Passport documentation, different authentication mechanisms are known as Strategies which can be installed and used modularly as desired. In this post we are using the Local Strategy. Toon Ketels does a good job of explaining the Passport authentication flow on his blog. The important information to note is that when a user submits their credentials, the request will be passed to Passport's authenticate() middleware we have established, and invoke which ever Strategy we have configured for that request route. These credentials (req.body.username and req.body.password) are passed into our Strategy and subjected to verification. If verified, the user will be serialized into the session and logged in to the application, if not verified the Strategy will return an error and the authenticate()middleware redirects to our designated failure location. This process looks like the following:

Passport authentication strategy

Now that we have an understanding of how Passport functions, let's build our Local Strategy! For our application, we will need to use two Local Strategies, one for registering new users (local-signup) and one for logging in already registered users (local-signin). We already have routes using each of these Strategies, and will just need to make sure to name our two Strategies to match their routes. Before we build our Strategies, we will need to build some helper functions for storing and retrieving users from MongoDB, as well as a creating a file to hold our various token information. We will start by uncommenting these lines in our index.js file:

// index.js/

var config = require('./config.js'), //config file contains all tokens and other private info
    funct = require('./functions.js'); //funct file contains our helper functions for our Passport and database work

Next we'll create our config.js file which will hold our MongoDB configuration. Replace "YOUR.MONGODB.HOST" with the IP address of your virtual server.

// config.js
// This file contains private configuration details.
// Do not add it to your Git repository.
module.exports = {
  "mongodbHost" : "YOUR.MONGODB.HOST"
};

With this information established we can create functions.js to hold our helper functions:

var bcrypt = require('bcryptjs'),
    Q = require('q'),
    config = require('./config.js'); //config file contains all tokens and other private info

// MongoDB connection information
var mongodbUrl = 'mongodb://' + config.mongodbHost + ':27017/users';
var MongoClient = require('mongodb').MongoClient

//used in local-signup strategy
exports.localReg = function (username, password) {
  var deferred = Q.defer();

  MongoClient.connect(mongodbUrl, function (err, db) {
    var collection = db.collection('localUsers');

    //check if username is already assigned in our database
    collection.findOne({'username' : username})
      .then(function (result) {
        if (null != result) {
          console.log("USERNAME ALREADY EXISTS:", result.username);
          deferred.resolve(false); // username exists
        }
        else  {
          var hash = bcrypt.hashSync(password, 8);
          var user = {
            "username": username,
            "password": hash,
            "avatar": "http://placepuppy.it/images/homepage/Beagle_puppy_6_weeks.JPG"
          }

          console.log("CREATING USER:", username);

          collection.insert(user)
            .then(function () {
              db.close();
              deferred.resolve(user);
            });
        }
      });
  });

  return deferred.promise;
};


//check if user exists
    //if user exists check if passwords match (use bcrypt.compareSync(password, hash); // true where 'hash' is password in DB)
      //if password matches take into website
  //if user doesn't exist or password doesn't match tell them it failed
exports.localAuth = function (username, password) {
  var deferred = Q.defer();

  MongoClient.connect(mongodbUrl, function (err, db) {
    var collection = db.collection('localUsers');

    collection.findOne({'username' : username})
      .then(function (result) {
        if (null == result) {
          console.log("USERNAME NOT FOUND:", username);

          deferred.resolve(false);
        }
        else {
          var hash = result.password;

          console.log("FOUND USER: " + result.username);

          if (bcrypt.compareSync(password, hash)) {
            deferred.resolve(result);
          } else {
            console.log("AUTHENTICATION FAILED");
            deferred.resolve(false);
          }
        }

        db.close();
      });
  });

  return deferred.promise;
}

The localReg function will be used in our Local Strategy to register new users and save their information into our database. It works by constructing a user object, including encrypting the password for protection. The function then checks if the user already exists in the database. If the user already exists it will reject the request by returning false to avoid overwriting the existing user. If the user does not already exist, the user object we created will be stored using its username as the key. This user will then be returned in order to be passed back to the Strategy so that it may proceed with the Verification process.

The localAuth function will be used to check if the user attempting to log in matches with the one stored in our database. This function checks the database for a user matching the given username. If a match is found, the retrieved password is compared to the one provided. If the passwords do not match the request is rejected by returning false. If the passwords do match, the user will then be returned in order to be passed back to the Strategy so that it may proceed with the Verification process.

Now that we have our helper functions, let's build our two Local Strategies! In the Passport section of index.js let's add:

// index.js/
//===============PASSPORT=================
// Use the LocalStrategy within Passport to login/"signin" users.
passport.use('local-signin', new LocalStrategy(
  {passReqToCallback : true}, //allows us to pass back the request to the callback
  function(req, username, password, done) {
    funct.localAuth(username, password)
    .then(function (user) {
      if (user) {
        console.log("LOGGED IN AS: " + user.username);
        req.session.success = 'You are successfully logged in ' + user.username + '!';
        done(null, user);
      }
      if (!user) {
        console.log("COULD NOT LOG IN");
        req.session.error = 'Could not log user in. Please try again.'; //inform user could not log them in
        done(null, user);
      }
    })
    .fail(function (err){
      console.log(err.body);
    });
  }
));
// Use the LocalStrategy within Passport to register/"signup" users.
passport.use('local-signup', new LocalStrategy(
  {passReqToCallback : true}, //allows us to pass back the request to the callback
  function(req, username, password, done) {
    funct.localReg(username, password)
    .then(function (user) {
      if (user) {
        console.log("REGISTERED: " + user.username);
        req.session.success = 'You are successfully registered and logged in ' + user.username + '!';
        done(null, user);
      }
      if (!user) {
        console.log("COULD NOT REGISTER");
        req.session.error = 'That username is already in use, please try a different one.'; //inform user could not log them in
        done(null, user);
      }
    })
    .fail(function (err){
      console.log(err.body);
    });
  }
));

The final pieces of our Passport section we need to add are the serialization and deserialization of users into and out of the session. Since our user objects are very simple we will be serializing and deserializing the entire user object, but as they become larger and more complex using only one aspect of the user object can be more efficient. For our needs inserting the following at the start of our Passport section will suffice:

// index.js/
//===============PASSPORT=================
// Passport session setup.
passport.serializeUser(function(user, done) {
  console.log("serializing " + user.username);
  done(null, user);
});

passport.deserializeUser(function(obj, done) {
  console.log("deserializing " + obj);
  done(null, obj);
});

As a side note, if you have any sections of your app you need to protect so that only authorized users can access them, Passport makes the process pretty easy. Simply use the following function on your route for the protection section:

// Simple route middleware to ensure user is authenticated.
function ensureAuthenticated(req, res, next) {
  if (req.isAuthenticated()) { return next(); }
  req.session.error = 'Please sign in!';
  res.redirect('/signin');
}

Now we have a working application with Local User Authentication! Let's test it out. If we start up our server and visit our specified port, we should be able to proceed to "Sign in" by clicking the big blue button or selecting the menu item. From here our "Sign in Locally" button should yield two further options to log in or create an account. Since we do not have any users in our database we will need to create an account. You can test your security by trying to log into a non-existent account. You should be denied. Once you create your account you should be logged in! If you head over to your MongoDB database you should also now have one user in "local-users."

Congratulations! You have Local Authorization!

Happy authorizing!

View the Code on GitHub