Securing MEVN Stack (Vue.js 2) Web Application using Passport

by Didin J. on Apr 07, 2018 Securing MEVN Stack (Vue.js 2) Web Application using Passport

Step by step tutorial on securing MEVN (MongoDB, Express.js, Vue.js 2, Node.js) Stack Web Application using Passport.js

A Comprehensive step by step tutorial on securing MEVN (MongoDB, Express.js, Vue.js 2, Node.js) Stack Web Application using Passport.js. Previously we have done securing MEAN and MERN Stack, now we'll show you how to do that with MEVN Stack. We are using same stories as the previous tutorial. Securing the RESTful API that accessible by Vue.js component or page.

The following tools, frameworks, and modules are required for this tutorial achievement:

- Node.js (use recommended version)
- Express.js
- MongoDB
- Mongoose.js
- Vue.js
- Vue-CLI
- Passport.js
- Terminal or Node.js command line
- IDE or Text Editor

We assume that you have already Installed Node.js and MongoDB. Make sure Node.js command line is working (on Windows) or runnable in Linux/OS X terminal. You can run MongoDB in the different terminal or command line. Type this command in the terminal or Node command line to check Node.js version.

node -v

And here is our Node.js version.

v8.9.4

Now, install Vue-CLI by typing this command.

sudo npm install -g vue-cli


1. Create a New Vue.js Application

To create a new Vue.js application using `vue-cli` simply type this command from terminal or Node command line.

vue init webpack mevn-secure

You will get the questions like below, just leave all by default.

? Project name mevn-secure
? Project description A Vue.js project
? Author didin jamaludin <[email protected]>
? Vue build standalone
? Install vue-router? Yes
? Use ESLint to lint your code? Yes
? Pick an ESLint preset Standard
? Set up unit tests Yes
? Pick a test runner jest
? Setup e2e tests with Nightwatch? Yes
? Should we run `npm install` for you after the project has been created? (recommended) npm

As, usual we have to make sure that Vue.js application running properly. Go to the new Vue.js application directory then run the application.

cd ./mevn-secure
npm run dev

Now, open the browser then go to this address `http://localhost:8080`. You will see this page if everything still on the right path.

Securing MEVN Stack (Vue.js 2) Web Application using Passport - Vue Home


2. Install Express.js as RESTful API Server

Close the running Vue.js app first by press `ctrl+c` then type this command for adding Express.js modules and it dependencies.

npm install --save express body-parser morgan body-parser serve-favicon

Next, create a new folder called `bin` then add a file called `www` on the root of Vue.js project folder.

mkdir bin
touch bin/www

Open and edit www file then add this lines of codes.

#!/usr/bin/env node

/**
 * Module dependencies.
 */

var app = require('../app');
var debug = require('debug')('mean-app:server');
var http = require('http');

/**
 * Get port from environment and store in Express.
 */

var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);

/**
 * Create HTTP server.
 */

var server = http.createServer(app);

/**
 * Listen on provided port, on all network interfaces.
 */

server.listen(port);
server.on('error', onError);
server.on('listening', onListening);

/**
 * Normalize a port into a number, string, or false.
 */

function normalizePort(val) {
  var port = parseInt(val, 10);

  if (isNaN(port)) {
    // named pipe
    return val;
  }

  if (port >= 0) {
    // port number
    return port;
  }

  return false;
}

/**
 * Event listener for HTTP server "error" event.
 */

function onError(error) {
  if (error.syscall !== 'listen') {
    throw error;
  }

  var bind = typeof port === 'string'
    ? 'Pipe ' + port
    : 'Port ' + port;

  // handle specific listen errors with friendly messages
  switch (error.code) {
    case 'EACCES':
      console.error(bind + ' requires elevated privileges');
      process.exit(1);
      break;
    case 'EADDRINUSE':
      console.error(bind + ' is already in use');
      process.exit(1);
      break;
    default:
      throw error;
  }
}

/**
 * Event listener for HTTP server "listening" event.
 */

function onListening() {
  var addr = server.address();
  var bind = typeof addr === 'string'
    ? 'pipe ' + addr
    : 'port ' + addr.port;
  debug('Listening on ' + bind);
}

Next, change the default server what run by `npm` command. Open and edit `package.json` then replace `start` value inside `scripts`.

"scripts": {
  "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
  "start": "npm run build && node ./bin/www",
  "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run",
  "e2e": "node test/e2e/runner.js",
  "test": "npm run unit && npm run e2e",
  "lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs",
  "build": "node build/build.js"
},

Now, create `app.js` in the root of project folder.

touch app.js

Open and edit `app.js` then add this lines of codes.

var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var bodyParser = require('body-parser');

var book = require('./routes/book');
var app = express();

app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({'extended':'false'}));
app.use(express.static(path.join(__dirname, 'dist')));
app.use('/books', express.static(path.join(__dirname, 'dist')));
app.use('/book', book);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  var err = new Error('Not Found');
  err.status = 404;
  next(err);
});

// restful api error handler
app.use(function(err, req, res, next) {
  console.log(err);

  if (req.app.get('env') !== 'development') {
      delete err.stack;
  }

    res.status(err.statusCode || 500).json(err);
});

module.exports = app;

Next, create routes folder then create routes file for the book.

mkdir routes
touch routes/book.js

Open and edit `routes/book.js` file then add this lines of codes.

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

/* GET home page. */
router.get('/', function(req, res, next) {
  res.send('Express RESTful API');
});

module.exports = router;

Now, run the server using this command.

npm start

You will see the previous Vue.js landing page when you point your browser to `http://localhost:3000`. When you change the address to `http://localhost:3000/book` you will see this page.

Securing MEVN Stack (Vue.js 2) Web Application using Passport - Express RESTful API

If you find this error.

Error: No default engine was specified and no extension was provided.

Then add to `app.js` this line after `app.use`.

app.set('view engine', 'html');


3. Install and Configure Mongoose.js

We need to access data from MongoDB. For that we will install and configure Mongoose.js. On the terminal type this command after stopping the running Express server.

npm install --save mongoose bluebird

Open and edit `app.js` then add this lines after another variable line.

var mongoose = require('mongoose');
mongoose.Promise = require('bluebird');
mongoose.connect('mongodb://localhost/mevn-secure', { promiseLibrary: require('bluebird') })
  .then(() =>  console.log('connection succesful'))
  .catch((err) => console.error(err));

Now, run MongoDB server on different terminal tab or command line or run from the service.

mongod

Next, you can test the connection to MongoDB run again the Node application and you will see this message on the terminal.

connection succesful

If you are still using built-in Mongoose Promise library, you will get this deprecated warning on the terminal.

(node:42758) DeprecationWarning: Mongoose: mpromise (mongoose's default promise library) is deprecated, plug in your own promise library instead: http://mongoosejs.com/docs/promises.html

That's the reason why we added `bluebird` modules and register it as Mongoose Promise library.


4. Create Mongoose.js Model

Add a models folder on the root of project folder for hold Mongoose.js model files.

mkdir models

Create new Javascript file that uses for Mongoose.js model. We will create a model of Book collection.

touch models/Book.js

Now, open and edit that file and add Mongoose require.

var mongoose = require('mongoose');

Then add model fields like this.

var BookSchema = new mongoose.Schema({
  isbn: String,
  title: String,
  author: String,
  description: String,
  published_year: String,
  publisher: String,
  updated_date: { type: Date, default: Date.now },
});

That Schema will mapping to MongoDB collections called book. If you want to know more about Mongoose Schema Datatypes you can find it here. Next, export that schema.

module.exports = mongoose.model('Book', BookSchema);


5. Create Routes for Accessing Book Data via Restful API

Open and edit again `routes/book.js` then replace all codes with this.

var express = require('express');
var router = express.Router();
var mongoose = require('mongoose');
var Book = require('../models/Book.js');

/* GET ALL BOOKS */
router.get('/', function(req, res, next) {
  Book.find(function (err, products) {
    if (err) return next(err);
    res.json(products);
  });
});

/* GET SINGLE BOOK BY ID */
router.get('/:id', function(req, res, next) {
  Book.findById(req.params.id, function (err, post) {
    if (err) return next(err);
    res.json(post);
  });
});

/* SAVE BOOK */
router.post('/', function(req, res, next) {
  Book.create(req.body, function (err, post) {
    if (err) return next(err);
    res.json(post);
  });
});

/* UPDATE BOOK */
router.put('/:id', function(req, res, next) {
  Book.findByIdAndUpdate(req.params.id, req.body, function (err, post) {
    if (err) return next(err);
    res.json(post);
  });
});

/* DELETE BOOK */
router.delete('/:id', function(req, res, next) {
  Book.findByIdAndRemove(req.params.id, req.body, function (err, post) {
    if (err) return next(err);
    res.json(post);
  });
});

module.exports = router;

Run again the Express server then open the other terminal or command line to test the Restful API by type this command.

curl -i -H "Accept: application/json" localhost:3000/book

If that command return response like below then REST API is ready to go.

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 2
ETag: W/"2-l9Fw4VUO7kr8CvBlt4zaMCqXZ0w"
Date: Tue, 03 Apr 2018 00:39:50 GMT
Connection: keep-alive

[]

Now, let's populate Book collection with initial data that sent from RESTful API. Run this command to populate it.

curl -i -X POST -H "Content-Type: application/json" -d '{ "isbn":"211333122, 98872233321123","title":"How to Build MEVN Stack","author": "Didin J.","description":"The comprehensive step by step tutorial on how to build MEVN (MongoDB, Express.js, Vue.js and Node.js) stack web application from scratch","published_year":"2017","publisher":"Djamware.com" }' localhost:3000/book

You will see this response to the terminal if success.

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 378
ETag: W/"17a-sHTmkPJsXHaJeRmgndSvsCg2L4E"
Date: Tue, 03 Apr 2018 00:41:00 GMT
Connection: keep-alive

{"_id":"5ac2cd9cf0cf24919a139d96","isbn":"211333122, 98872233321123","title":"How to Build MEVN Stack","author":"Didin J.","description":"The comprehensive step by step tutorial on how to build MEVN (MongoDB, Express.js, Vue.js and Node.js) stack web application from scratch","published_year":"2017","publisher":"Djamware.com","updated_date":"2018-04-03T00:41:00.310Z","__v":0}


6. Install and Configure Passport.js

It's time for securing RESTful API. First, install Passport.js module and its dependencies.

npm install --save bcrypt-nodejs jsonwebtoken morgan passport passport-jwt

Add configuration files to `config` folder.

touch config/settings.js
touch config/passport.js

Open and edit `config/settings.js` then add this lines of codes.

module.exports = {
  'secret':'mevnsecure'
};

That file holds a secret code for generating JWT token. Next, open and edit `config/passport.js` then add this lines of codes.

var JwtStrategy = require('passport-jwt').Strategy,
    ExtractJwt = require('passport-jwt').ExtractJwt;

// load up the user model
var User = require('../models/user');
var settings = require('../config/settings'); // get settings file

module.exports = function(passport) {
  var opts = {};
  opts.jwtFromRequest = ExtractJwt.fromAuthHeaderWithScheme("jwt");
  opts.secretOrKey = settings.secret;
  passport.use(new JwtStrategy(opts, function(jwt_payload, done) {
    User.findOne({id: jwt_payload.id}, function(err, user) {
          if (err) {
              return done(err, false);
          }
          if (user) {
              done(null, user);
          } else {
              done(null, false);
          }
      });
  }));
};

This config is used for getting user by matching JWT token with token get from the client. This configuration needs to create User model. For that, create a new Javascript file inside models folder.

touch models/User.js

Open and edit `models/User.js` then add this lines of codes.

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

var UserSchema = new Schema({
  username: {
        type: String,
        unique: true,
        required: true
    },
  password: {
        type: String,
        required: true
    }
});

UserSchema.pre('save', function (next) {
    var user = this;
    if (this.isModified('password') || this.isNew) {
        bcrypt.genSalt(10, 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();
            });
        });
    } else {
        return next();
    }
});

UserSchema.methods.comparePassword = function (passw, cb) {
    bcrypt.compare(passw, this.password, function (err, isMatch) {
        if (err) {
            return cb(err);
        }
        cb(null, isMatch);
    });
};

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

The different of User models are an additional function for creating an encrypted password using `Bcrypt` and function for comparing encrypted password.


7. Create Routers for RESTful API Authentication

Now, it's time for the real game. We will create Router for authenticating the user and restrict resources. In routes, folder creates new Javascript file.

touch routes/auth.js

Open and edit `routes/auth.js` then declares all required variables.

var mongoose = require('mongoose');
var passport = require('passport');
var settings = require('../config/settings');
require('../config/passport')(passport);
var express = require('express');
var jwt = require('jsonwebtoken');
var router = express.Router();
var User = require("../models/user");

Create router for signup or register the new user.

router.post('/register', function(req, res) {
  if (!req.body.username || !req.body.password) {
    res.json({success: false, msg: 'Please pass username and password.'});
  } else {
    var newUser = new User({
      username: req.body.username,
      password: req.body.password
    });
    // save the user
    newUser.save(function(err) {
      if (err) {
        return res.json({success: false, msg: 'Username already exists.'});
      }
      res.json({success: true, msg: 'Successful created new user.'});
    });
  }
});

Create router for login or sign-in.

router.post('/login', function(req, res) {
  User.findOne({
    username: req.body.username
  }, function(err, user) {
    if (err) throw err;

    if (!user) {
      res.status(401).send({success: false, msg: 'Authentication failed. User not found.'});
    } else {
      // check if password matches
      user.comparePassword(req.body.password, function (err, isMatch) {
        if (isMatch && !err) {
          // if user is found and password is right create a token
          var token = jwt.sign(user.toJSON(), settings.secret);
          // return the information including token as JSON
          res.json({success: true, token: 'JWT ' + token});
        } else {
          res.status(401).send({success: false, msg: 'Authentication failed. Wrong password.'});
        }
      });
    }
  });
});

Export the router variable as a module.

module.exports = router;

Open and edit `app.js` again to mapping authentication router to URL. Add this require variable after book variable.

var auth = require('./routes/auth');

Add `app.use` after book `app.use`.

app.use('/api/auth', auth);


8. Securing the Book RESTful API Router

It's time to securing the RESTful API resources. We will add a restriction for Book RESTful API. For that, open and edit `routes/book.js` then add this require variables.

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

Replace all save book router with this.

router.post('/', passport.authenticate('jwt', { session: false}), function(req, res) {
  var token = getToken(req.headers);
  if (token) {
    Book.create(req.body, function (err, post) {
      if (err) return next(err);
      res.json(post);
    });
  } else {
    return res.status(403).send({success: false, msg: 'Unauthorized.'});
  }
});

Replace all get books router with this.

router.get('/', passport.authenticate('jwt', { session: false}), function(req, res) {
  var token = getToken(req.headers);
  if (token) {
    Book.find(function (err, books) {
      if (err) return next(err);
      res.json(books);
    });
  } else {
    return res.status(403).send({success: false, msg: 'Unauthorized.'});
  }
});

Add function to get and extract JWT token.

getToken = function (headers) {
  if (headers && headers.authorization) {
    var parted = headers.authorization.split(' ');
    if (parted.length === 2) {
      return parted[1];
    } else {
      return null;
    }
  } else {
    return null;
  }
};

Now, run again the Express server then test the modified Book router by sending the same request.

curl -i -H "Accept: application/json" localhost:3000/book

It should response this messages.

HTTP/1.1 401 Unauthorized
X-Powered-By: Express
Date: Tue, 03 Apr 2018 01:12:03 GMT
Connection: keep-alive
Content-Length: 12

Unauthorized

That's mean the Book RESTful API is secure and accessible to an authorized user.


9. Install and Configure Required Modules for Vue.js

We need additional modules for accessing RESTful API and styling the front end. For navigating or routing between views, there already installed `vue-router` module. Type this commands to install the required modules.

npm i bootstrap-vue [email protected]
npm i --save axios

Open and edit `src/main.js` then add the imports for Bootstrap-Vue.

import Vue from 'vue'
import BootstrapVue from 'bootstrap-vue'
import App from './App'
import router from './router'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'

Add this line after `Vue.config`.

Vue.use(BootstrapVue)

To register or create routes for the whole application navigation, open and edit `src/router/index.js` then replace all codes with this.

import Vue from 'vue'
import Router from 'vue-router'
import BookList from '@/components/BookList'
import Login from '@/components/Login'
import Register from '@/components/Register'

Add the router to each component or page.

export default new Router({
  routes: [
    {
      path: '/',
      name: 'BookList',
      component: BookList
    },
    {
      path: '/login',
      name: 'Login',
      component: Login
    },
    {
      path: '/register',
      name: 'Register',
      component: Register
    }
  ]
})

Add Vue files for above registered components or pages.

touch src/components/BookList.vue
touch src/components/Login.vue
touch src/components/Register.vue


10. Modify Book List Component

Now, open and edit `src/components/BookList.vue` then add this lines of codes.

<template>
  <b-row>
    <b-col cols="12">
      <h2>
        Book List
        <b-link href="#/add-book">(Add Book)</b-link>
      </h2>
      <b-table striped hover :items="books" :fields="fields">
        <template slot="actions" scope="row">
          <b-btn size="sm" @click.stop="details(row.item)">Details</b-btn>
        </template>
      </b-table>
      <ul v-if="errors && errors.length">
        <li v-for="error of errors">
          {{error.message}}
        </li>
      </ul>
    </b-col>
  </b-row>
</template>

<script>

import axios from 'axios'

export default {
  name: 'BookList',
  data () {
    return {
      fields: {
        isbn: { label: 'ISBN', sortable: true, 'class': 'text-center' },
        title: { label: 'Book Title', sortable: true },
        actions: { label: 'Action', 'class': 'text-center' }
      },
      books: [],
      errors: []
    }
  },
  created () {
    axios.defaults.headers.common['Authorization'] = localStorage.getItem('jwtToken')
    axios.get(`http://localhost:3000/book`)
    .then(response => {
      this.books = response.data
    })
    .catch(e => {
      this.errors.push(e)
      if(e.response.status === 401) {
        this.$router.push({
          name: 'Login'
        })
      }
    })
  },
  methods: {
    details (book) {
      this.$router.push({
        name: 'ShowBook',
        params: { id: book._id }
      })
    }
  }
}
</script>

As you can see, there's a call to Book RESTful API when the page is loaded. If API calls response `401` error status then it will be redirected to the login page. We also put a logout button inside the condition where JWT token does not exist in local storage.


11. Modify Login Component

Previously we have created a Vue file for login component. Now, open and edit `src/components/Login.vue` then add/replace this lines of codes.

<template>
  <b-row class="justify-content-md-center">
    <b-col cols="6">
      <div v-if="errors && errors.length">
        <div v-for="error of errors">
          <b-alert show>{{error.message}}</b-alert>
        </div>
      </div>
      <b-form @submit="onSubmit">
        <b-form-group id="fieldsetHorizontal"
                  horizontal
                  :label-cols="4"
                  breakpoint="md"
                  label="Enter Username">
          <b-form-input id="username" :state="state" v-model.trim="login.username"></b-form-input>
        </b-form-group>
        <b-form-group id="fieldsetHorizontal"
                  horizontal
                  :label-cols="4"
                  breakpoint="md"
                  label="Enter Password">
          <b-form-input type="password" id="password" :state="state" v-model.trim="login.password"></b-form-input>
        </b-form-group>
        <b-button type="submit" variant="primary">Login</b-button>
        <b-button type="button" variant="success" @click.stop="register()">Register</b-button>
      </b-form>
    </b-col>
  </b-row>
</template>

<script>

import axios from 'axios'

export default {
  name: 'Login',
  data () {
    return {
      login: {},
      errors: []
    }
  },
  methods: {
    onSubmit (evt) {
      evt.preventDefault()
      axios.post(`http://localhost:3000/api/auth/login/`, this.login)
      .then(response => {
        localStorage.setItem('jwtToken', response.data.token)
        this.$router.push({
          name: 'BookList'
        })
      })
      .catch(e => {
        console.log(e)
        this.errors.push(e)
      })
    },
    register () {
      this.$router.push({
        name: 'Register'
      })
    }
  }
}
</script>


12. Modify Register Component

Previously we have created a Javascript file for register component. Now, open and edit `src/components/Register.vue` then add/replace this lines of codes.

<template>
  <b-row class="justify-content-md-center">
    <b-col cols="6">
      <h2>Please Register</h2>
      <div v-if="errors && errors.length">
        <div v-for="error of errors">
          <b-alert show>{{error.message}}</b-alert>
        </div>
      </div>
      <b-form @submit="onSubmit">
        <b-form-group id="fieldsetHorizontal"
                  horizontal
                  :label-cols="4"
                  breakpoint="md"
                  label="Enter Username">
          <b-form-input id="username" :state="state" v-model.trim="register.username"></b-form-input>
        </b-form-group>
        <b-form-group id="fieldsetHorizontal"
                  horizontal
                  :label-cols="4"
                  breakpoint="md"
                  label="Enter Password">
          <b-form-input type="password" id="password" :state="state" v-model.trim="register.password"></b-form-input>
        </b-form-group>
        <b-button type="submit" variant="primary">Register</b-button>
        <b-button type="button" variant="success" @click="$router.go(-1)">Cancel</b-button>
      </b-form>
    </b-col>
  </b-row>
</template>

<script>

import axios from 'axios'

export default {
  name: 'Register',
  data () {
    return {
      register: {},
      errors: []
    }
  },
  methods: {
    onSubmit (evt) {
      evt.preventDefault()
      axios.post(`http://localhost:3000/api/auth/register/`, this.register)
      .then(response => {
        alert("Registered successfully")
        this.$router.push({
          name: 'Login'
        })
      })
      .catch(e => {
        console.log(e)
        this.errors.push(e)
      })
    },
  }
}
</script>

On that codes, we use the same view with login component. Register action will redirect to login component after successful registration.

Finally, for logout function just add logout button inside the Book List page and set the action just for clear local storage with the item `jwtToken`. Open and edit again `src/components/BookList.vue` then add logout function to the method.

methods: {
  details (book) {
    this.$router.push({
      name: 'ShowBook',
      params: { id: book._id }
    })
  },
  logout () {
    localStorage.removeItem('jwtToken')
    this.$router.push({
      name: 'Login'
    })
  }
}

Also add the button next to the header.

<h2>Book List
  <b-link @click="logout()">(Logout)</b-link>
</h2>


13. Run and Test the MEVN Stack Secure Application

It's a time for test the whole MEVN stack secure application. The Vue 2 application will integrate with Express.js by building the Vue 2 application first. Type this command to build the Vue 2 application then run the Express.js server.

npm start

Open the browser then point to `localhost:3000`. You will see the default landing page redirected to the login page.

Securing MEVN Stack (Vue.js 2) Web Application using Passport - Login Page

Here's the register page.

Securing MEVN Stack (Vue.js 2) Web Application using Passport - Register Page

And the main page.

Securing MEVN Stack (Vue.js 2) Web Application using Passport - Book List

That's it, the complete tutorial of securing the MEVN stack (Vue.js 2) web application using Passport.js authentication. If there's something wrong with the steps of the tutorial, you can compare it with the full working source code from our GitHub.

For more detailed on Node.js, you can take the following course:

Thanks!

The following resources might be useful for you:

Loading…