16 minute read

User authentication REST API using Node and MongoDB

Apr 7, 2018 / Javascript

Last Updated: Friday, October 12th, 2018

Part 1 – Project Introduction

Chris Anderson from Microsoft once described Javascript as the “English of Programming Languages” – a lot of people can speak at least a little bit of it, even it it’s bad. It’s not a perfect language, but it’s a language which is relatively easy to learn for a lot of people.

I wanted to become more familiar with Javascript and Node.js by building a simple user authentication module using passport-jwt for user account tokenization. I found a great video by Brad Traversy on YouTube and mostly followed his guide to get myself up and running.

I’ll be building a simple REST API using Node.js, Express, MongoDB, and Mongoose. Eventually, I will integrate this application with an Angular front end, but for now I will leave just the backend on my GitHub for anybody to use as a very simple authentication template.

To test the functionality without the front end Angular UI, I’ll be using Postman, but you can use almost any application capable of sending HTTP requests with a body.

For communicating with the database, I’ll be using Automattic’s Mongoose ODM.

For those more familiar with ORM (object relational mapping): An ORM maps between an Object Model and a Relational Database. An ODM maps between an Object Model and a Document Database. MySQL is not an ORM, it’s a Relational Database, more specifically, a SQL Database. ActiveRecord is an example of an ORM for Ruby on Rails. MongoDB is not an ODM, it’s a Document Database. Mongoose is the ODM.

Part 2 – MongoDB Installation

First, we’ll need to install MongoDB locally. To do this:

  1. Open your terminal application. I’m using iTerm2
  2. I’m assuming that you have Homebrew installed. If you don’t, do yourself a favor.
  3. Once Homebrew is installed , run brew update to make sure everything is up to date.
  4. Run brew install mongodb to install MongoDB
  5. Since we’re using MongoDB locally, you’ll need to create the database directory on your machine where your Mongo data will live. You can create this directory in its default location by running mkdir -p /data/db
    If that doesn’t work, you’re probably missing some permissions for writing files. Run sudo mkdir -p /data/db instead.
  6. Make sure your /data/db directory has the right read/write permissions by running:
    $ sudo chown -R `id -un` /data/db
    $ # Enter your password
  7. Now that you’ve successfully installed and configured MongoDB, let’s run the Mongo daemon to start the Mongo server, and confirm you’ve installed everything flawlessly.
  8. NOTE – I’m running this application on MacOS, but if you are on Linux Ubuntu or any Linux version, you’ll have to take this step to ensure your MongoDB service is running, and to do that you can just run sudo service mongodb start
  9. Back to MacOS. Type mongod to start the Mongo daemon.
  10. In a separate terminal window, type mongo to run the Mongo shell.
  11. In the Mongo shell, type db.version() to see what version of Mongo you’re running. If you don’t see any errors, you’re all set!
  12. To exit the Mongo shell, type quit()
  13. To stop the Mongo daemon, hit ctrl-c

You don’t need to worry too much about opening the Mongo Shell again until we have some data to play with; for now, we’ll only be concerned with the Mongo daemon. If the Mongo daemon isn’t running when you try to make requests to your database, you’ll hit a brick wall and your data will not be mapped properly.

Now, we can start to set up our folder structure.

Part 3 (The Beef) – Folder Structure & Initial Routes

First, we’re going to go ahead and generate our package.json file; go ahead and mkdir mean-auth-app in whichever directory you choose to hold your project. Next, run npm init and follow the on-screen instructions to create the package file. (I’m naming my entry point app.js).

Open the folder in your text editor of choice, which is VSCode for me. Open that package.json file and we’re going to start off by creating a start script and adding it in the ‘script’ object:
"start":"node app"

It’s worth noting that we’ll be using nodemon for development. While we’re using nodemon, we won’t need to run app.js using the node command, but we’ll leave the script there for future use.

Next, copy this list of dependecies:

"dependencies": {
    "express": "*",
    "mongoose": "*",
    "bcryptjs": "*",
    "cors": "*",
    "jsonwebtoken": "*",
    "body-parser": "*",
    "passport-jwt": "*",
    "passport": "*"
  }

Save the file, head back to your terminal, and run npm install.

Once installed, you can close your package.json file and create an app.js file in the root directory of your project to serve as your main entry point file. You’ll want to import all of the modules you need for this file, found below:

const express    = require('express');
const path       = require('path');
const bodyParser = require('body-parser');
const cors       = require('cors');
const passport   = require('passport');
const mongoose   = require('mongoose');
const config     = require('./config/database');
Below your imports, initialize your app variable:
const app        = express();
Right under that, create a variable for my port number so that it is easily accessible and can be modified from one location in your file:
const port       = 3000;

Then go ahead and use app.listen to tell your app which port to listen for:

app.listen(port, () => {
  console.log('VOLK: Server started on port: ' +port);
});

The only reason I included the VOLK: before the log is to help me distinguish which console.logs result from my code vs. other messages that my application throws in there.

Now you should have enough code in your entry file to allow you to successfully run your server to check if you’ve run into any bugs at this point. Use the start script you created earlier, node app from the terminal.

Now, in your browser you should see some message alerting you that your browser “Cannot GET /” or something similar. To temporarily get rid of the error:

app.get('/', (req, res) => {
  res.send('Invalid End Point');
});

Now we want to install Nodemon globally.

npm install -g nodemon

Once installed, run nodemon from within your application directory and wait for your ‘Server listening…’ message to let you know your app is running on the port you specified earlier.

So that was a lot, but your app.js file should look something like the stuff below, champ:

const express    = require('express');
const path       = require('path');
const bodyParser = require('body-parser');
const cors       = require('cors');
const passport   = require('passport');
const mongoose   = require('mongoose');
const config     = require('./config/database');

const app        = express();

const port       = 3000;

app.listen(port, () => {
  console.log('VOLK: Server started on port: ' +port);
});

app.get('/', (req, res) => {
  res.send('Invalid End Point');
});

Next, we’ll go ahead and integrate our CORS middleware, so that we can make requests to this API from a different domain name. I don’t know nearly enough to dive into web security, but the MDN docs have a great article diving deep into the intricacies of CORS with plenty of examples.

Since we installed the CORS npm module in the beginning of this project, integrating the CORS middleware is as simple as calling:

app.use(cors());

Somewhere in your entry file; I choose to include it right under my port variable. The module basically does us the favor of injecting different headers within our application using res.header, but you can read some more on that here. We’ll be using this module with another module we installed called body-parser, which parses incoming request bodies. For example, when you receive a form submission, body-parser will help you parse the form input data. When you receive a GET request with a string query, body-parser will help you parse that URL parameter for validation.

app.use(bodyParser.json());

Next, let’s make use of the Express router so that we can encapsulate all of the user routes in another file without cluttering our main entry file. Go ahead and create a new constant called ‘users’ and set it to equal the following:

const users = require('./routes/users');

Go ahead and place that variable toward the top of your entry file with the other require()’s (good code hygiene 😉). Then, we’re going to call app.use again under your bodyParser.json().

app.use('/users', users);

Create a new directory in your project to match that route we required just now, call it ‘routes’ and save it. Next, create a new file in that directory called ‘users.js’ and require the following modules:

const express = require('express');
const router = express.Router();

Next, we’ll create the three routes that users should have the ability to interact with; a registration route, an authentication route, and a profile route (which we will eventually protect via the JWT tokenization).

// Register
router.post('/register', (req, res, next) => {
  res.send("REGISTER");
});

// Authenticate
router.get('/authenticate', (req, res, next) => {
  res.send("AUTHENTICATE");
});

// Profile
router.get('/profile', (req, res, next) => {
  res.send("PROFILE");
});
Of course, the parameters being passed into the .send methods are just placeholders, we’ll go ahead and fill those out in just a minute. For now, navigate back to your entry file, we’ll need to tell our application where to find our static assets.
app.use(express.static(path.join(__dirname, 'public')));

Create a directory called ‘public’ and leave it alone for now.

Now, we’re going to want to call the ‘connect’ function from Mongoose to connect to our database. You’d usually pass the location of the database as an argument to the function we’re about to call, however we are going to create a config folder to hold the database to stay organized.

Go ahead and create a new directory called ‘config’ and a file within that new directory called ‘database.js’. At the top of that file:

module.exports = { 
  database: 'mongodb://localhost:27017/meanauth',
  secret: 'yoursecret' 
}

The ‘database’ key points to the default location of the Mongo DB, which usually sits in port 27017. Following the port, we’ll want to name our project, for now I’ll just call it meanauth.

Head back to your entry file, and in the top of that file we’ll need to require the module we just exported from the ‘config’ directory.

const config = require('./config/database');

And then, for the ‘connect’ function we want to call from Mongoose:

mongoose.connect(config.database);

Now what we’ll want to do is to put in a message to let us know if we are connected to that database:

mongoose.connection.on('connected', () => {
  console.log('VOLK: Connected to database' +config.database);
});

Save that file, and nodemon should have refreshed with a new console log letting you know you’re connected to the database found in your config file. We can also add another message beneath the line we just wrote to let us know if there is a database error that we need to take care of.

mongoose.connection.on('error', (err) => {
  console.log('VOLK: Database error: ' +err);
});

Part 4 – User Model

Next we’ll be creating our user model file to handle data such as name, password, email, and username. We’ll also have our functions that interact with the database in that file.

Create a directory called models/ and require the following modules:

const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const config = require('../config/database');

Now let’s create the user schema:

const UserSchema = mongoose.Schema({
  name: {
    type: String
  },
  email: {
    type: String,
    required: true
  },
  username: {
    type: String,
    required: true
  },
  password: {
    type: String,
    required: true
  }
});

const User = module.exports = mongoose.model('User', UserSchema);

Let’s go ahead and create two functions in this file; one to retrieve users by userID and another function to retrieve users by username.

module.exports.getUserById = function(id, callback) {
  User.mongoose.findById(id, callback)
}

module.exports.getUserByUsername = function(username, callback) {
  const query = { username: username };
  User.mongoose.findOne(query, callback)
}

 Part 5 – The ‘REGISTER’ Path

Back in the root directory of your file navigate to routes/users.js/ and require the schema we created in part 4 to the top of the file:

const User = require('../models/user');

We should also import passport, the JSON web token module, and the config file from the config/database we created:

const passport = require('passport');
const jwt = require('jsonwebtoken');
const config = require('../config/database');

 

Navigate down to the router.post request for the ‘/REGISTER’ path and change the callback function body to:

router.post('/register', (req, res, next) => {
  let newUser = new User({
    name: req.body.name,
    email: req.body.email, 
    username: req.body.username,
    password: req.body.password,
  });

  User.addUser(newUser, (err, user) => {
    if (error) {
      res.json({ success: false, msg: 'Failed to register user.' });
    } else {
      res.json({ success: true, msg: 'User registered!'});
    }
  });
});

Head back to models/user.js and add the addUser function at the bottom of the file to export:

module.exports.addUser = function (newUser, callback) {
  // Hash User Password
  // The first argument is an int and it represents the number of rounds of hashing
  bcrypt.genSalt(10, (err, salt) => {
    bcrypt.hash(newUser.password, salt, (err, hash) => {
      if (err) throw err
      newUser.password = hash;
      newUser.save(callback)
    });
  });
};

Part 6 – Try it Out

At this point, we can go ahead and pass an HTTP POST request to send our application some registration data to ensure that the application properly saves it.

I’ll be using Postman, but feel free to user any other utility that is capable of sending HTTP requests, even curl.

Using Postman, make a POST request to http://localhost:3000/user/register and then for your header parameters, go ahead and pass a Content-Type as a JSON object that looks like:

{
  "name":"John Doe",
  "email":"jdoe@gmail.com",
  "username":"john",
  "password":"123456"
}

After squashing a few bugs, you should receive a response body that looks like:

{
  "success": true,
  "msg": "User registered!"
}

Part 7 – Authentication

In this part, we will set up passport with a JWT strategy to authenticate users and receive tokens.

In our app.js/ file, under the Body Parser middleware:

app.use(passport.initialize());
app.use(passport.session());

Now, we are going to choose a strategy we’d like to use for the Passport tokenization.

In your config/ folder, create a file called passport.js.
At the top of that file, require the following:

const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;
const User = require('../models/user');
const config = require('../config/database');

Then, export the following at the bottom of the file:

module.exports = function(passport) {
  let opts = {};
  opts.jwtFromRequest = ExtractJwt.fromAuthHeader();
  opts.secretOrKey = config.secret;
  passport.use(new JwtStrategy(opts, (jwt_payload, done) => {
    User.getUserById(jwt_payload._id, (err, user) => {
      if (err) {
        return done(err, false);
      }
      
      if (user) {
        return done(null, user);
      } else {
        return done(null, false);
      }
    };
  }));
};

You’ll want to make sure you include the export above inside of your app.js file. I’m putting my require statement under the Passport Middleware where we specified passport.initialize() and passport.session():

require('./config/passport')(passport);

Now, you should save the files we were just editing and go check nodemon to go make sure that nodemon isn’t breaking. Once you’ve squashed any possible bugs, we are now going to our routes/ directory into the users.js file.

Modify the post request to the authentication route:

router.post('/authenticate', (req, res, next) => {
  const username = req.body.username;
  const password = req.body.password;

  User.getUserByUsername(username, (err, user) => {
    if (err) throw err;
    if (!user) {
      return res.json({ success: false, msg: "User not found!" });
    }

    User.comparePassword(password, user.password, (err, isMatch) => {
      if (err) throw err;
      if (isMatch) {
        const token = jwt.sign(user, config.secret, {
          expiresIn: 604800, // 1 week in seconds
        });

        res.json({
          success: true,
          token: 'JWT ' + token,
          user: {
            id: user._id,
            name: user.name,
            username: user.username,
            email: user.email
          }
        });
      } else {
        return res.json({ success: false, msg: "Wrong password!" });
      }
    });
  });
});

Quick explanation of the code above; first we’re going to check if the username exists. If the username does exist, then we are going to take the password and try to match it to the matched password. If the password matches, we are going to assign the user an authentication token that expires in a week, and then return some JSON containing a JSON Web Token ID to the request origin. If the password does not match, the user is not authenticated and will have to re-enter their password.

We also created a function we have not yet defined, called User.comparePassword(). Let’s go ahead and create this function in our User model, as to uphold the separation of concerns between files we have stayed true to throughout this project and make sure everything is encapsulated properly.

At the bottom of the models/user.js file:

module.exports.comparePassword = function(candidatePassword, hash callback) {
  bcrypt.compare(candidatePassword, hash, (err, isMatch) => {
    if (err) throw err;
    callback(null, isMatch);
  });
};

Alright, we should be okay now. That was quite a bit of code we just wrote, but let’s go ahead and try to see if it still works.

Open Postman, open a new tab and start drafting a new POST request. the address is going to be http://localhost:3000/users/authenticate. For the request HEADER, set Content-Type once again to application/json. For the body:

{
  "username":"john",
  "password":"123456"
}

If all goes well, you should see something that looks like:

{
  "success": true,
  "token": "JWT eyJHFJDKVjfkFHJKLEHVJKVnjkVNjkd39057JKDLFjkl7FJKDFLHJkhJkhfjekidvnrinuvdsjkl7853JHIELHuinjkfelnvuincivnsjkLuiHUGLnjKVLDNJNEIFLNVJDKn49329JKELNGiNJEKGRNnkef7FnjKVLn9FNJEK3klJdnJKGLRGLNGJKgNJkg7GjnklgrnjKLgejnkl",
  "user": {
    "id": "58a345a628f6455ab2912b2",
    "name": "John Doe",
    "username": "john",
    "email":"jdoe@gmail.com"
  }
}

Eventually, when we integrate this API with Angular 2, we will be able to store the token into either a cookie, or the localStorage object.

Conclusion

All in all, I really enjoyed this little project. I was so extremely frustrated after getting stuck on this issue: https://github.com/matthewvolk/user-auth-node-app/issues/1 for a few days, only to realize I had forgotten to import the actual passport module on a dependency file.

I’m not sure when I’ll be finishing this project, as I have recently discovered the joys of Python. I’m hoping to pick this back up sometime during the Summer of 2018.

Until then, stay RESTful 😉