Refactoring a Basic, Authenticated API with Node, Express, and Mongo

In a previous series, I was attempting to break down building a full stack JavaScript application. I have since learned a great deal, and I want to go back and make improvements upon what was already covered before moving forward. This means the whole app will be restructured, so it might behoove you to start from scratch. This post will be at a slightly quicker pace than the previous three guides I wrote, so reference those if need be.

Once again, our chosen stack for development will be: Node, Express, React (with Redux), and MongoDB.

Step 1: Scaffolding

Open up a new folder and call it saas-tutorial. Run git init. Feel free to add your license of choice and add a README.md if you'd like. I recommend adding a .gitignore file here as well. For the most part, I used the standard GitHub Node.js boilerplate, but I added the following under "#Dependency directory":

client/node_modules  
server/node_modules  

Next, create a folder called client and a folder called server. We will go ahead and get our directory structure in place for the client-side of the app now, but we won't touch that in this part of the tutorial. Within the client folder, create the following:

/src
|----/actions
|----/components
|----/public
     |----/stylesheets
     |----/img
|----/reducers

Now in the server folder, create the following:

/config
|----/main.js
/controllers
/models
index.js  
package.json  
router.js  

That's it for step one. Let's start our server.

Step 2: Starting a Node/Express Server

Navigate to the server folder and run npm init. Answer the questions when you are prompted. Name the package in a way that signifies this is your server-side code. Next, run:

npm install -g nodemon  

This will globally install nodemon, which you will use to reload your server when changes have been made. Now we can install the local packages we will need:

npm install --save express morgan  

The --save tells NPM to add those packages to your package.json file automatically. Now, open config/main.js and add the following:

module.exports = {  
'port': process.env.PORT || 3000  
}

Save the file, then move onto index.js. The first step is to import the packages we'll need in addition to the config file we just created.

// Importing Node modules and initializing Express
const express = require('express'),  
      app = express(),
      logger = require('morgan'),
      config = require('./config/main');

Now we can start the server by adding this just below the imports:

// Start the server
const server = app.listen(config.port);  
console.log('Your server is running on port ' + config.port + '.');  

We will also want to set up basic middleware for all our server requests. Underneath where we started the server, add:

// Setting up basic middleware for all Express requests
app.use(logger('dev')); // Log requests to API using morgan

// Enable CORS from client-side
app.use(function(req, res, next) {  
  res.header("Access-Control-Allow-Origin", "*");
  res.header('Access-Control-Allow-Methods', 'PUT, GET, POST, DELETE, OPTIONS');
  res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization, Access-Control-Allow-Credentials");
  res.header("Access-Control-Allow-Credentials", "true");
  next();
});

Now if you run nodemon index.js, you'll be running a server! This was all covered in more depth in this post. Please refer there or leave a comment if you want a deeper understanding of what we are doing in this step. Please note, there will be differences in code. This guide is making improvements to the old one.

Step 3: Adding JWT Authentication and Connecting to MongoDB

This step is going to be similar to this post, so please refer there if you want more information or get confused along the way.

For this step, we will need to add more packages. Run:

npm install --save body-parser mongoose  

Now open up config/main.js again and add your MongoDB connection details. I recommend using mLab for a free MongoDB sandbox, but you can also set a local MongoDB instance up if you choose.

We are also going to add the secret we will use for signing our JSON Web Tokens (JWT) down the road) while we are already in this file. You should now have something like this:

module.exports = {  
  // Secret key for JWT signing and encryption
  'secret': 'super secret passphrase',
  // Database connection information
  'database': 'mongodb://localhost:27017',
  // Setting port for server
  'port': process.env.PORT || 3000
}

Now we'll go back into index.js and add imports for mongoose and body-parser. Your import section should now look like this:

const express = require('express'),  
      app = express(),
      bodyParser = require('body-parser'),
      logger = require('morgan'),
      mongoose = require('mongoose'),
      config = require('./config/main');

Since we have imported mongoose and our config, we can connect our server to our database by adding this below the imports:

// Database Connection
mongoose.connect(config.database);  

In our middleware section, we will want to add body-parser, so we can parse urlencoded bodies to JSON and expose the object in req.body when we start building endpoints.

app.use(bodyParser.urlencoded({ extended: false }));  
app.use(bodyParser.json());  

Now let's think about how we'll structure our user information. Let's move over to the models folder and create a new file, user.js. In this file, we will be defining the schema by which our user data will be saved.

First, we will need to run:

npm install --save bcrypt-nodejs  

Import the following:

const mongoose = require('mongoose'),  
      Schema = mongoose.Schema,
      bcrypt = require('bcrypt-nodejs');

Now we can actually start building our schema. To gain a deeper understanding of what we can do with schemas, take a look at the mongoose documentation.

Enter the following below the imports:

//================================
// User Schema
//================================
const UserSchema = new Schema({  
  email: {
    type: String,
    lowercase: true,
    unique: true,
    required: true
  },
  password: {
    type: String,
    required: true
  },
  profile: {
    firstName: { type: String },
    lastName: { type: String }
  },
  role: {
    type: String,
    enum: ['Member', 'Client', 'Owner', 'Admin'],
    default: 'Member'
  },
  resetPasswordToken: { type: String },
  resetPasswordExpires: { type: Date }
},
{
  timestamps: true
});

Hopefully you can start to figure out what we're accomplishing here. We are saying that the user objects we create and save will follow this structure. If a field is required, a new user cannot be saved without it. The "enum" key means that field can only be saved with one of the specified values. Likewise, with type, only data of the specified type can be saved for that field.

Next, we need to handle password hashing. A plain-text password should never be stored in a database. That's a security problem just waiting to happen. We will use bcrypt to take care of this:

// Pre-save of user to database, hash password if password is modified or new
UserSchema.pre('save', function(next) {  
  const user = this,
        SALT_FACTOR = 5;

  if (!user.isModified('password')) return next();

  bcrypt.genSalt(SALT_FACTOR, function(err, salt) {
    if (err) return next(err);

    bcrypt.hash(user.password, salt, null, function(err, hash) {
      if (err) return next(err);
      user.password = hash;
      next();
    });
  });
});

Now we need to create a method to check a user's password (on a login attempt) against the hashed password we have stored.

Fortunately, bcrypt makes this simple:

// Method to compare password for login
UserSchema.methods.comparePassword = function(candidatePassword, cb) {  
  bcrypt.compare(candidatePassword, this.password, function(err, isMatch) {
    if (err) { return cb(err); }

    cb(null, isMatch);
  });
}

Finally, we can export the model and move onto our next task.

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

Next up, we'll configure passport, which is a very popular and flexible authentication library. We will need to install several more packages.

npm install --save passport passport-jwt passport-local`  

Now, let's import passport, our main config, and our user model into a new file, config/passport.js:

// Importing Passport, strategies, and config
const passport = require('passport'),  
      User = require('../models/user'),
      config = require('./main'),
      JwtStrategy = require('passport-jwt').Strategy,
      ExtractJwt = require('passport-jwt').ExtractJwt,
      LocalStrategy = require('passport-local');

Underneath that, we will tell passport that we have opted to use the email field rather than the username field:

const localOptions = { usernameField: 'email' };  

Now, we will set up the local login strategy, which will be used to authenticate users with an email address and password. A successful local login will yield the user a JSON Web Token to use to authenticate future requests automatically.

This next portion is essentially straight from the passport documentation:

// Setting up local login strategy
const localLogin = new LocalStrategy(localOptions, function(email, password, done) {  
  User.findOne({ email: email }, function(err, user) {
    if(err) { return done(err); }
    if(!user) { return done(null, false, { error: 'Your login details could not be verified. Please try again.' }); }

    user.comparePassword(password, function(err, isMatch) {
      if (err) { return done(err); }
      if (!isMatch) { return done(null, false, { error: "Your login details could not be verified. Please try again." }); }

      return done(null, user);
    });
  });
});

Now, let's set up the JWT authentication options:

const jwtOptions = {  
  // Telling Passport to check authorization headers for JWT
  jwtFromRequest: ExtractJwt.fromAuthHeader(),
  // Telling Passport where to find the secret
  secretOrKey: config.secret
};

Now, we can set up our JWT login strategy and pass our options through:

// Setting up JWT login strategy
const jwtLogin = new JwtStrategy(jwtOptions, function(payload, done) {  
  User.findById(payload._id, function(err, user) {
    if (err) { return done(err, false); }

    if (user) {
      done(null, user);
    } else {
      done(null, false);
    }
  });
});

Please note, some people have had issues with this step. Depending on your setup, you might need to replace payload._id with payload.doc._id or payload.document._id. When in doubt, add console.log(payload); to your code and search the console for the right user ID if you are always getting the same user back when logging in different user accounts.

And finally, allow passport to use the strategies we defined:

passport.use(jwtLogin);  
passport.use(localLogin);  

We have made some great progress, but now we need to build our authentication controller. Create and open controllers/authentication.js. I thought we could have some fun here and use some (very light) ES6 to start familiarizing with it. We will use ES6 heavily when we get to the client-side portion of this app. We will use "let" and "const" rather than "var". Add "use strict" to the top of the file. Now we need to install a couple more packages.

npm install --save jsonwebtoken crypto  

Let's import them, as well as our main config and our user model:

const jwt = require('jsonwebtoken'),  
      crypto = require('crypto'),
      User = require('../models/user'),
      config = require('../config/main');

We need to create a function to generate a JSON web token from the user object we pass in.

function generateToken(user) {  
  return jwt.sign(user, config.secret, {
    expiresIn: 10080 // in seconds
  });
}

We don't want to use the entire user object to sign our JWTs-- that's a lot of information to eventually store in a cookie. Plus, we don't want to be returning huge blocks of what could be sensitive user information. We need control. Let's create a function to select the user information we want to pass through:

// Set user info from request
function setUserInfo(request) {  
  return {
    _id: request._id,
    firstName: request.profile.firstName,
    lastName: request.profile.lastName,
    email: request.email,
    role: request.role,
  };

Now we need to create handlers for the login and registration routes. There's a lot of error checking involved. It's a decent amount of code, but it's pretty easy to follow, so don't be scared:

//========================================
// Login Route
//========================================
exports.login = function(req, res, next) {

  let userInfo = setUserInfo(req.user);

  res.status(200).json({
    token: 'JWT ' + generateToken(userInfo),
    user: userInfo
  });
}


//========================================
// Registration Route
//========================================
exports.register = function(req, res, next) {  
  // Check for registration errors
  const email = req.body.email;
  const firstName = req.body.firstName;
  const lastName = req.body.lastName;
  const password = req.body.password;

  // Return error if no email provided
  if (!email) {
    return res.status(422).send({ error: 'You must enter an email address.'});
  }

  // Return error if full name not provided
  if (!firstName || !lastName) {
    return res.status(422).send({ error: 'You must enter your full name.'});
  }

  // Return error if no password provided
  if (!password) {
    return res.status(422).send({ error: 'You must enter a password.' });
  }

  User.findOne({ email: email }, function(err, existingUser) {
      if (err) { return next(err); }

      // If user is not unique, return error
      if (existingUser) {
        return res.status(422).send({ error: 'That email address is already in use.' });
      }

      // If email is unique and password was provided, create account
      let user = new User({
        email: email,
        password: password,
        profile: { firstName: firstName, lastName: lastName }
      });

      user.save(function(err, user) {
        if (err) { return next(err); }

        // Subscribe member to Mailchimp list
        // mailchimp.subscribeToNewsletter(user.email);

        // Respond with JWT if user was created

        let userInfo = setUserInfo(user);

        res.status(201).json({
          token: 'JWT ' + generateToken(userInfo),
          user: userInfo
        });
      });
  });
}

For some added fun, we can prepare the authorization/role check handler too:

//========================================
// Authorization Middleware
//========================================

// Role authorization check
exports.roleAuthorization = function(role) {  
  return function(req, res, next) {
    const user = req.user;

    User.findById(user._id, function(err, foundUser) {
      if (err) {
        res.status(422).json({ error: 'No user was found.' });
        return next(err);
      }

      // If user is found, check role.
      if (foundUser.role == role) {
        return next();
      }

      res.status(401).json({ error: 'You are not authorized to view this content.' });
      return next('Unauthorized');
    })
  }
}

We will have to create "forgot password" and "reset password" routes in the future, but we'll worry about that when we need to.

The last step for the authentication in this guide will be to create some routes. Back in the main server folder, create a new file, router.js. We need to import our controller, express, passport, and our passport config file here.

const AuthenticationController = require('./controllers/authentication'),  
      express = require('express'),
      passportService = require('./config/passport'),
      passport = require('passport');

Next, we need to setup our passport middleware:

// Middleware to require login/auth
const requireAuth = passport.authenticate('jwt', { session: false });  
const requireLogin = passport.authenticate('local', { session: false });  

If you'd like to add a role-based authorization system, you could create constant references for the role names:

// Constants for role types
const REQUIRE_ADMIN = "Admin",  
      REQUIRE_OWNER = "Owner",
      REQUIRE_CLIENT = "Client",
      REQUIRE_MEMBER = "Member";

And finally, we set up the routes:

module.exports = function(app) {  
  // Initializing route groups
  const apiRoutes = express.Router(),
        authRoutes = express.Router();

  //=========================
  // Auth Routes
  //=========================

  // Set auth routes as subgroup/middleware to apiRoutes
  apiRoutes.use('/auth', authRoutes);

  // Registration route
  authRoutes.post('/register', AuthenticationController.register);

  // Login route
  authRoutes.post('/login', requireLogin, AuthenticationController.login);

// Set url for API group routes
  app.use('/api', apiRoutes);
};

Now we just need to give our app access to these routes. In index.js, import your routes:

const router = require('./router');  

Lastly, add this at the very bottom of the index.js file:

router(app);  

Using Postman, you should now be able to test these endpoints. Instructions for running these tests using Postman can be found in the original post here.

The next part (a RESTful API for a chat system) will be broken into another post. We are going to refactor what I originally put together pretty thoroughly, and this feels like a pretty good breaking point. For anyone who would like to see a sample of what we are going to put together, I am a little further along on this repository, though I have more refactoring to do there as well.

Here is the React/Redux (client side) portion of this tutorial: http://blog.slatepeak.com/build-a-react-redux-app-with-json-web-token-jwt-authentication/

Joshua Slate

Entrepreneur. Rock climber. Software engineer. Founder of @SlatePeak and others.

Saint Paul, MN

Subscribe to SlatePeak

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!