Secure REST API using NodeJS : Part 2 – Express + JWT + Mongoose | Alexander Molochko

All modern programming languages have at least one framework for implementing a REST API. What language and technology to choose depends on a lot of factors: type of a project, estimated budget, existing infrastructure, ability to scale horizontally and many more. MEAN stack and all NodeJS related stuff are rapidly becoming popular, as a result there are a great number of tools you can use to speed up your development process. In the previous part I described the basics of RESTFul API design and Security. This part of the series describes how to build a simple blog API with NodeJS and Express + JWT + Mongoose as a primary set of tools.

Prerequisites

This tutorial part assumes that you have a basic knowledge of NodeJS/Ecmascript programming, experience of working with NodeJS command line utilities like npm and setting up development environment.

Tools

For building a REST API the following tools are used in this tutorial:

  • Express.js – A web framework and routes handler.
  • Mongoose – A object modeling tool that leverages MongoDB for data persistence.
  • Passport strategy – A library that allows to create custom authentication strategies for the Passport library.
  • JSONWebToken – An implementation of JSON Web Token.
  • Express Validator – A middleware for validating requests.
  • Validator.js – A validation library.

MongoDB

We will use MongoDB for storing data. So you need to have the mongodb daemon running locally or remotely. Personally I prefer using Docker for such purpose in order to keep my local machine clean. Here is the docker command I use to run the latest MongoDb container with mapping the container’s database directory to the local one. Therefore the data won’t be lost after restarting the container.

1
docker run --name mongo_db_blog -p 28000:27017 -v /Volumes/Work/Development/NodeJS/BlogAPI/DataStore:/data/db -d mongo:latest

NodeJS and NPM

Of course the core technology is NodeJS bundled with Node Package Manager (NPM). There are a lot of installation guides out there, so I think it is not necessary to describe the installation process in this blog post.

There are only two simple steps to do this. Download and install NodeJS, it has NPM built in, after that run the following command to get the latest NPM version.

1
npm install npm@latest -g

1. Initial project setup

In order to create a new project you can use predefined templates, but I encourage you to build an app from scratch in order to understand the purpose of each file. But of course you are free to use the interactive shell command to generate a boilerplate package.json file.

1
2
cd BlogAPI
npm init .

For this tutorial the final package.json file will look as follows. But of course you can change it to fit your needs.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
{
  "name": "blog-api",
  "version": "1.0.0",
  "description": "An example of Blog API using NodeJS, Mongoose, Express and JWT",
  "main": "index.js",
  "repository": {
    "type": "git",
    "url": "[email protected]/CROSP/blog-api-nodejs.git"
  },
  "keywords": [
    "Blog",
    "API",
    "Express",
    "Example",
    "Mongoose"
  ],
  "author": "Alexander CROSP Molochko",
  "license": "MIT",
  "dependencies": {
    "auto-bind": "^1.1.0",
    "body-parser": "^1.17.1",
    "debug": "^2.6.6",
    "express": "latest",
    "express-validator": "^3.2.0",
    "extend-error": "0.0.2",
    "http-status-codes": "^1.1.6",
    "jsonwebtoken": "^7.4.0",
    "mongoose": "^4.9.8",
    "passport": "^0.3.2",
    "passport-jwt": "^2.2.1",
    "passport-local": "latest",
    "passport-strategy": "latest",
    "should": "^11.2.1",
    "validator": "^7.0.0"
  }
}

Project directory structure

The final project directory structure will be similar.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
- app/
----- authstrategy/
----- base/
----- controller/
----- error/
----- handler/
----- manager/
----- model/
----- route/
- config/
----- index.js
- package.json

Note that this is only directory structure all files are omitted to fit the screen :). You can choose a different directory structure, but it is always a good practice to separate concerns. This directory structure is not the best as well, and I prefer to use the package by feature structure style, however for the sake of simplicity I will use the structure described above throughout the tutorial.

Setting global variables and configuration

First of all, let’s define some global variables for paths and configuration constants.

We need to define a connection url for MongoDB. Create a file called db.js inside the config directory. In my case I am running MongoDB inside the local docker container, so url is defined like this.

1
2
3
module.exports = {
    MONGO_CONNECT_URL:"mongodb://127.0.0.1:28000/blogapi"
};

In order to keep our paths in one place and avoid using relative paths like ../../model, define the following variables in global-paths.js file inside the same directory.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
global.APP_MODEL_PATH = APP_ROOT_PATH + 'model/';
global.APP_AUTH_STRATEGY = APP_ROOT_PATH + 'authstrategy/';
global.APP_CONTROLLER_PATH = APP_ROOT_PATH + 'controller/';
global.APP_HANDLER_PATH = APP_ROOT_PATH + 'handler/';
global.APP_SERVICE_PATH = APP_ROOT_PATH + 'service/';
global.APP_BASE_PACKAGE_PATH = APP_ROOT_PATH + 'base/';
global.APP_ERROR_PATH = APP_ROOT_PATH + 'error/';
global.APP_MANAGER_PATH = APP_ROOT_PATH + 'manager/';
global.APP_MIDDLEWARE_PATH = APP_ROOT_PATH + 'middleware/';
global.APP_ROUTE_PATH = APP_ROOT_PATH + 'route/';
global.CONFIG_BASE_PATH = __dirname + '/';

The next step is to create the entry point file for our application, let’s name it index.js and place it in the root directory of the project.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Root path
global.APP_ROOT_PATH = __dirname + '/app/';
// Set other app paths
require('./config/global-paths');
// Set config variables
global.config = require('./config');

// Create an Express App
const express = require('express');
const app = express();
// Include dependencies
const bodyParser = require('body-parser');
const mongoose = require('mongoose');
const routes = require(APP_ROUTE_PATH);
const ValidationManager = require(APP_MANAGER_PATH + 'validation');
const validationManager = new ValidationManager();
// Connect to DB
mongoose.Promise = global.Promise;
mongoose.connect(config.db.MONGO_CONNECT_URL);
// Use json formatter middleware
app.use(bodyParser.json());

// Set Up validation middleware
app.use(validationManager.provideDefaultValidator());
// Setup routes
app.use('/', routes);

app.listen(global.config.server.PORT, function () {
    console.log('App is running on ' + global.config.server.PORT);
});

Here is the explanation of the code above.

  • const app = express();: Create an express app object, that is the main object that handles all user requests, makes decisions where to route a request, etc.
  • mongoose.Promise = global.Promise;: The Mongoose library now has the default Promise implementation deprecated, so we need to plugin custom implementation. This is the pluggable design principle.
  • mongoose.connect(config.db.MONGO_CONNECT_URL);: Establish a connection to the mongodb database.
  • app.use(bodyParser.json()): Use the Body Parser JSON middleware. As far as we are going to use the JSON format for communication, this middleware helps to transparently use JSON objects without manually converting them.
  • app.use(validationManager.provideDefaultValidator()): Use the default validator from the ValidationManager class, that is responsible for providing different validators. This part will be explained in a while.
  • app.use(‘/’, routes);: Set the route handler for the base url. This will be discussed extensively later in this article.
  • Finally, start the server and listen for incoming requests.

Understanding concepts

A middleware in this case is the implementation of the Chain Of Responsibility pattern. Middleware handlers sit between incoming requests and an actual route handling function (for example in a controller). So this is handled transparently by the framework in a chain. This allows to put common logic for all requests into a middleware, so we don’t need to repeat it for every request, like body to JSON conversion, authentication, etc. Furthermore, if a middleware layer is built correctly, you don’t need to validate any additional, and mostly not related, logic inside a handling function, hence the fact this function is called mean that all barriers were passed successfully.

The Validation Manager class is used as a factory that provides validators, in this case we have only one validator, however there could be more. So in order to separate responsibilities and objects creation this is a separate class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const BaseAutoBindedClass = require(APP_BASE_PACKAGE_PATH + 'base-autobind');
const expressValidator = require('express-validator');
class ValidationManager extends BaseAutoBindedClass {
    constructor() {
        super();
        this._validator = require('express-validator');
    }

    provideDefaultValidator() {
        return expressValidator({
            errorFormatter: ValidationManager.errorFormatter
        })
    }

    static errorFormatter(param, msg, value) {
        let namespace = param.split('.'),
            root = namespace.shift(),
            formParam = root;

        while (namespace.length) {
            formParam += '[' + namespace.shift() + ']';
        }
        return {
            param: formParam,
            msg: msg,
            value: value
        };
    }
}
module.exports = ValidationManager;

The BaseAutoBindedClass class

Before go any further, I need to explain why I am using BaseAutoBindedClass as a parent for most of classes. The problem is that the this keyword in Javascript is sometime a bit tricky to use, especially in ECMAScript 6 with classes, when method is passed as a callback. It depends on a context where a function(method) is called from. You are probably aware of this if you have ever worked with Javascript.

There is a small module that just binds all methods in an object to itself or a specified context.

1
2
3
4
5
6
7
const autoBind = require('auto-bind');
class BaseAutoBindedClass {
    constructor() {
        autoBind(this);
    }
}
module.exports = BaseAutoBindedClass;

2. Defining routes

Now it is the time to define routes for our application. There are will be two types of resources the user resource and the blog post resource. As a result we will define the following endpoints.

The User endpoint

HTTP Method URI Description
GET /users/ Get all users
GET /users/:id Get user info by id
POST /users/ Create a new user

The Blog Post endpoint

HTTP Method URI Description
GET /posts/ Get all posts
GET /posts/:id Get the content of a post
POST /posts/ Create a blog post
PUT /posts/:id Update a post
DELETE /posts/:id Delete a post

The Auth endpoint

HTTP Method URI Description
POST /auth/ Authenticate a user and issue a JWT token

Having our routes defined we can start implementing them in code. I will use the following directory structure for routes.

1
2
3
4
5
6
- route/
----- v1/
---------- auth.js
---------- index.js
---------- post.js
---------- user.js

As you can see I am using file names equivalent to the endpoints names, this structure is equivalent to the URL path, that makes it convenient for navigation and finding a desired route in a project.

Let’s start by creating the index.js file.

1
2
3
4
5
6
7
8
const express = require('express'),
    router = express.Router();
const ROUTE_V1_PATH = APP_ROUTE_PATH + "v1/";
router.use('/auth', require(ROUTE_V1_PATH + 'auth'));
router.use('/users', require(ROUTE_V1_PATH + 'user'));
router.use('/posts', require(ROUTE_V1_PATH + 'post'));

module.exports = router;

Index.js file

When including modules using the require function, under the hood it tries to find the exact file and if there is a folder with the such name, it will look for an index.js file in it. As an example, in the main index.js file of the project, I am using this approach for including routes const routes = require(APP_ROUTE_PATH);.

Implementation of route modules is quite simple in our case, this is just delegation of requests to controllers.

The user.js route file.

1
2
3
4
5
6
7
8
9
const router = require('express').Router();
const UserController = require(APP_CONTROLLER_PATH + 'user');
let userController = new UserController();

router.get('/', userController.get);
router.get('/:id', userController.getUserInfo);
router.post('/', userController.post);

module.exports = router;

The post.js route file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const router = require('express').Router();
const PostController = require(APP_CONTROLLER_PATH + 'post');
let postController = new PostController();

router.get('/', postController.get);
router.get('/:id', postController.getPostContent);
router.post('/', postController.post);
router.delete('/:id', postController.del);
router.put('/:id', postController.put);

module.exports = router;

And finally the auth.js route file.

1
2
3
4
5
6
7
const router = require('express').Router();
const AuthController = require(APP_CONTROLLER_PATH + 'auth');
let authController = new AuthController();

router.post('/', authController.post);

module.exports = router;

3. Defining Models

For accessing the MongoDB database we will use Mongoose, an object data modeling library for MongoDB, this is much easier than writing raw queries, since you think in terms of JavaScript objects rather than database semantics.

The file structure will be the following.

1
2
3
- model/
----- post.js
----- user.js

The User schema

Let’s define the User schema.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
let mongoose = require('mongoose');
let Schema = mongoose.Schema;
const crypto = require('crypto');
let UserSchema = new Schema({
    firstName: String,
    lastName: String,
    salt: {
        type: String,
        required: true
    },
    isActive: {type: Boolean, default: true},
    dateCreated: {type: Date, default: Date.now},
    email: String,
    hashedPassword: {
        type: String,
        required: true,
    },
});
UserSchema.methods.toJSON = function () {
    let obj = this.toObject();
    delete obj.hashedPassword;
    delete obj.__v;
    delete obj.salt;
    return obj
};

UserSchema.virtual('id')
    .get(function () {
        return this._id;
    });
UserSchema.virtual('password')
    .set(function (password) {
        this.salt = crypto.randomBytes(32).toString('base64');
        this.hashedPassword = this.encryptPassword(password, this.salt);
    })
    .get(function () {
        return this.hashedPassword;
    });

UserSchema.methods.encryptPassword = function (password, salt) {
    return crypto.createHmac('sha1', salt).update(password).digest('hex');
};
UserSchema.methods.checkPassword = function (password) {
    return this.encryptPassword(password, this.salt) === this.hashedPassword;
};
module.exports.UserModel = mongoose.model('User', UserSchema);

Some explanations for the code above. In the toJSON function, we are removing secret fields that should not be returned when user info is requested. There are some other ways, but I’ve found this the most convenient.

There is the the password virtual property defined, that has the getter and the setter functions. In this case the setter function, rather than store a password in plaintext, creates a hashed, salted version of the password provided as an argument. In addition we implement the checkPassword method, that compares a password provided in plain text by a user, with a password stored in the database.

The Blog Post schema

The second schema we need is the Blog Post schema. It is simpler than the User schema.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const ObjectId = Schema.ObjectId;
let BlogPostSchema = new Schema({
    title: String,
    content: String,
    authorId: {
        type: ObjectId,
        required: true
    },
    dateCreated: {type: Date, default: Date.now},
    dateModified: {type: Date, default: Date.now},
});
BlogPostSchema.pre('update', function (next, done) {
    this.dateModified = Date.now();
    next();
});
BlogPostSchema.pre('save', function (next, done) {
    this.dateModified = Date.now();
    next();
});
BlogPostSchema.methods.toJSON = function () {
    let obj = this.toObject();
    delete obj.__v;
    return obj
};
module.exports.BlogPostModel = mongoose.model('Post', BlogPostSchema);

Later in this tutorial we will define one more schema. Also I am using internal autogenerated mongo _id property as the primary identifier for objects. It is not always a good idea to follow this way, so you may have to add a custom ID property to a schema definition.

4. Implementing use cases and handlers

At this stage it would be reasonable to implement controllers, but we will put the authentication strategies inside controllers, therefore it requires more effort, so we will leave this part for later discussion. Now we will implement our logic for retrieving data and CRUD operations.

You may also be wondering why not just implement everything in the controller layer. The idea is to separate concerns and do not mix up everything in one single layer or even a function. This may seem redundant for this example, but I am trying always to keep my apps modular and loosely coupled.

In our case handlers are classes containing Business Rules that are forming the core of the application. This is similar to the concept of the Use Case layer from Clean Architecture. However for the sake of simplicity not all rules and best practices are followed, only base ideas. Furthermore the term handler can be a little bit confusing, but think of it like a group of related use cases.

The User Handler

Let’s start by creating a UserHandler class, it will be responsible for creating new users and retrieving existing ones.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
const UserModel = require(APP_MODEL_PATH + 'user').UserModel;
const AlreadyExistsError = require(APP_ERROR_PATH + 'already-exists');
const ValidationError = require(APP_ERROR_PATH + 'validation');
const UnauthorizedError = require(APP_ERROR_PATH + 'unauthorized');

class UserHandler {
    constructor() {
        this._validator = require('validator');
    }

    static get USER_VALIDATION_SCHEME() {
        return {
            'firstName': {
                notEmpty: true,
                isLength: {
                    options: [{min: 2, max: 15}],
                    errorMessage: 'First getName must be between 2 and 15 chars long'
                },
                errorMessage: 'Invalid First Name'
            },
            'lastName': {
                notEmpty: true,
                isLength: {
                    options: [{min: 2, max: 15}],
                    errorMessage: 'Lastname must be between 2 and 15 chars long'
                },
                errorMessage: 'Invalid First Name'
            },
            'email': {
                isEmail: {
                    errorMessage: 'Invalid Email'
                },
                errorMessage: "Invalid email provided"
            },
            'password': {
                notEmpty: true,
                isLength: {
                    options: [{min: 6, max: 35}],
                    errorMessage: 'Password must be between 6 and 35 chars long'
                },
                errorMessage: 'Invalid Password Format'
            }

        };
    }

    getUserInfo(req, userToken, callback) {
        req.checkParams('id', 'Invalid user id provided').isMongoId();
        req.getValidationResult()
            .then((result) => {
                if (!result.isEmpty()) {
                    let errorMessages = result.array().map(function (elem) {
                        return elem.msg;
                    });
                    throw new ValidationError('There have been validation errors: ' + errorMessages.join(' && '));
                }

                let userId = req.params.id;
                if (userToken.id !== req.params.id) {
                    throw new UnauthorizedError("Provided id doesn't match with  the requested user id")
                }
                else {
                    return new Promise(function (resolve, reject) {
                        UserModel.findById(userId, function (err, user) {
                            if (user === null) {

                            } else {
                                resolve(user);
                            }
                        });
                    });
                }

            })
            .then((user) => {
                callback.onSuccess(user);
            })
            .catch((error) => {
                callback.onError(error);
            });
    }

    createNewUser(req, callback) {
        let data = req.body;
        let validator = this._validator;
        req.checkBody(UserHandler.USER_VALIDATION_SCHEME);
        req.getValidationResult()
            .then(function (result) {
                if (!result.isEmpty()) {
                    let errorMessages = result.array().map(function (elem) {
                        return elem.msg;
                    });
                    throw new ValidationError('There are validation errors: ' + errorMessages.join(' && '));
                }
                return new UserModel({
                    firstName: validator.trim(data.firstName),
                    lastName: validator.trim(data.lastName),
                    email: validator.trim(data.email),
                    password: validator.trim(data.password)
                });
            })
            .then((user) => {
                return new Promise(function (resolve, reject) {
                    UserModel.find({email: user.email}, function (err, docs) {
                        if (docs.length) {
                            reject(new AlreadyExistsError("User already exists"));
                        } else {
                            resolve(user);
                        }
                    });
                });
            })
            .then((user) => {
                user.save();
                return user;
            })
            .then((saved) => {
                callback.onSuccess(saved);
            })
            .catch((error) => {
                callback.onError(error);
            });
    }
}

module.exports = UserHandler;

This code probably requires explanation. To begin, the USER_VALIDATION_SCHEME constant is an object that contains validation rules for the data being sent, in this case, for creating a new user. This is possible thanks to a great library – express-validator. This approach is the very convenient declarative way to get rid of the if else hell.

I am using Promises extensively in this project. Because of the asynchronous nature of NodeJS, the Promise pattern is a really great way to handle chains of async function calls without getting lost in the callback hell. If you have never heard about this concept, I strongly encourage you to read about it and start using Promises.

The callback parameter is used along with Promises since sometimes we need to handle exceptions directly in a handler without propagating them to a controller. Surely, you are free to return a promise from this method call and handle it in a controller.

I think both of these methods are easy to understand. The last thing I want to pay attention to is the req parameter. This is a request object that is passed through different layers starting from the routing layer. Generally, this is not a good idea to pass request through different layers, and at best our use case/handler layer shouldn’t know anything about requests, http and other implementation details, but again for the sake of simplicity I am passing it directly to the use case layer.

The Blog Post Handler

Next, define BlogPostHandler class, implementing all CRUD operations for the blog post model.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
const BlogPostModel = require(APP_MODEL_PATH + 'post').BlogPostModel;
const ValidationError = require(APP_ERROR_PATH + 'validation');
const NotFoundError = require(APP_ERROR_PATH + 'not-found');
const BaseAutoBindedClass = require(APP_BASE_PACKAGE_PATH + 'base-autobind');

class BlogPostHandler extends BaseAutoBindedClass {
    constructor() {
        super();
        this._validator = require('validator');
    }

    static get BLOG_POST_VALIDATION_SCHEME() {
        return {
            'title': {
                notEmpty: true,
                isLength: {
                    options: [{min: 10, max: 150}],
                    errorMessage: 'Post title must be between 2 and 150 chars long'
                },
                errorMessage: 'Invalid post title'
            },
            'content': {
                notEmpty: true,
                isLength: {
                    options: [{min: 50, max: 3000}],
                    errorMessage: 'Post content must be between 150 and 3000 chars long'
                },
                errorMessage: 'Invalid post content'
            },
            'authorId': {
                isMongoId: {
                    errorMessage: 'Invalid Author Id'
                },
                errorMessage: "Invalid email provided"
            }
        };
    }

    createNewPost(req, callback) {
        let data = req.body;
        let validator = this._validator;
        req.checkBody(BlogPostHandler.BLOG_POST_VALIDATION_SCHEME);
        req.getValidationResult()
            .then(function (result) {
                if (!result.isEmpty()) {
                    let errorMessages = result.array().map(function (elem) {
                        return elem.msg;
                    });
                    throw new ValidationError('There are validation errors: ' + errorMessages.join(' && '));
                }
                return new BlogPostModel({
                    title: validator.trim(data.title),
                    content: validator.trim(data.content),
                    authorId: data.authorId,
                });
            })
            .then((user) => {
                user.save();
                return user;
            })
            .then((saved) => {
                callback.onSuccess(saved);
            })
            .catch((error) => {
                callback.onError(error);
            });
    }

    deletePost(req, callback) {
        let data = req.body;
        req.checkParams('id', 'Invalid post id provided').isMongoId();
        req.getValidationResult()
            .then(function (result) {
                    if (!result.isEmpty()) {
                        let errorMessages = result.array().map(function (elem) {
                            return elem.msg;
                        });
                        throw new ValidationError('There are validation errors: ' + errorMessages.join(' && '));
                    }
                    return new Promise(function (resolve, reject) {
                        BlogPostModel.findOne({_id: req.params.id}, function (err, post) {
                            if (err !== null) {
                                reject(err);
                            } else {
                                if (!post) {
                                    reject(new NotFoundError("Post not found"));
                                }
                                else {
                                    resolve(post);
                                }
                            }
                        })
                    });
                }
            )
            .then((post) => {
                post.remove();
                return post;
            })
            .then((saved) => {
                callback.onSuccess(saved);
            })
            .catch((error) => {
                callback.onError(error);
            });
    }

    updatePost(req, callback) {
        let data = req.body;
        let validator = this._validator;
        req.checkBody(BlogPostHandler.BLOG_POST_VALIDATION_SCHEME);
        req.getValidationResult()
            .then(function (result) {
                    if (!result.isEmpty()) {
                        let errorMessages = result.array().map(function (elem) {
                            return elem.msg;
                        });
                        throw new ValidationError('There are validation errors: ' + errorMessages.join(' && '));
                    }
                    return new Promise(function (resolve, reject) {
                        BlogPostModel.findOne({_id: req.params.id}, function (err, post) {
                            if (err !== null) {
                                reject(err);
                            } else {
                                if (!post) {
                                    reject(new NotFoundError("Post not found"));
                                }
                                else {
                                    resolve(post);
                                }
                            }
                        })
                    });
                }
            )
            .then((post) => {
                post.content = validator.trim(data.content);
                post.title = validator.trim(data.title);
                post.save();
                return post;
            })
            .then((saved) => {
                callback.onSuccess(saved);
            })
            .catch((error) => {
                callback.onError(error);
            });
    }

    getSinglePost(req, callback) {
        let data = req.body;
        req.checkParams('id', 'Invalid post id provided').isMongoId();
        req.getValidationResult()
            .then(function (result) {
                    if (!result.isEmpty()) {
                        let errorMessages = result.array().map(function (elem) {
                            return elem.msg;
                        });
                        throw new ValidationError('There are validation errors: ' + errorMessages.join(' && '));
                    }
                    return new Promise(function (resolve, reject) {
                        BlogPostModel.findOne({_id: req.params.id}, function (err, post) {
                            if (err !== null) {
                                reject(err);
                            } else {
                                if (!post) {
                                    reject(new NotFoundError("Post not found"));
                                }
                                else {
                                    resolve(post);
                                }
                            }
                        })
                    });
                }
            )
            .then((post) => {
                callback.onSuccess(post);
            })
            .catch((error) => {
                callback.onError(error);
            });
    }

    getAllPosts(req, callback) {
        let data = req.body;
        new Promise(function (resolve, reject) {
            BlogPostModel.find({}, function (err, posts) {
                if (err !== null) {
                    reject(err);
                } else {
                    resolve(posts);
                }
            });
        })
            .then((posts) => {
                callback.onSuccess(posts);
            })
            .catch((error) => {
                callback.onError(error);
            });
    }
}

module.exports = BlogPostHandler;

The implementation is similar to UserHandler, we are validating input data whenever it is passed in a request and then perform CRUD operations using Mongoose models and finally pass the result back to a controller.

Later we will also add one more handler for authentication.

5. Implementing controllers

Now having our handlers defined, let’s get back to controllers. First we will define the BaseController class.

The Base Controller

Let’s by creating a BaseController class, it will be some sort of an “abstract” class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const ResponseManager = require(APP_MANAGER_PATH + 'response');
const BaseAutoBindedClass = require(APP_BASE_PACKAGE_PATH + 'base-autobind');

class BaseController extends BaseAutoBindedClass {
    constructor() {
        super();
        if (new.target === BaseController) {
            throw new TypeError("Cannot construct BaseController instances directly");
        }
        this._responseManager = ResponseManager;
    }

    getAll(req, res) {

    }

    get(req, res) {

    }

    create(req, res) {

    }

    update(req, res) {

    }

    remove(req, res) {

    }

    authenticate(req, res, callback) {

    }
}
module.exports = BaseController;

The main purpose of this class is to define some kind of a common interface for all controllers to implement. I follow the “one controller per REST resource” rule. CRUD methods are related to a single resource item (for example a blog post), in addition to retrieve all items there is the getAll method.

Meanwhile, there is one more important method – authenticate. This method should be implemented by subclasses or just call a callback immediately if no authentication is required by a controller.

Response Manager

There is an instance of the ResponseManager class being created in BaseController. This manager implements default logic for generating responses, furthermore it provides boilerplate static methods for handling errors and success results.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
const HttpStatus = require('http-status-codes');
const BasicResponse = {
    "success": false,
    "message": "",
    "data": {}
};

class ResponseManager {
    constructor() {

    }

    static get HTTP_STATUS() {
        return HttpStatus;
    }

    static  getDefaultResponseHandler(res) {
        return {
            onSuccess: function (data, message, code) {
                ResponseManager.respondWithSuccess(res, code || ResponseManager.HTTP_STATUS.OK, data, message);
            },
            onError: function (error) {
                ResponseManager.respondWithError(res, error.status || 500, error.message || 'Unknown error');
            }
        };
    }

    static  getDefaultResponseHandlerData(res) {
        return {
            onSuccess: function (data, message, code) {
                ResponseManager.respondWithSuccess(res, code || ResponseManager.HTTP_STATUS.OK, data, message);
            },
            onError: function (error) {
                ResponseManager.respondWithErrorData(res, error.status, error.message, error.data);
            }
        };
    }

    static  getDefaultResponseHandlerError(res, successCallback) {
        return {
            onSuccess: function (data, message, code) {
                successCallback(data, message, code);
            },
            onError: function (error) {
                ResponseManager.respondWithError(res, error.status || 500, error.message || 'Unknown error');
            }
        };
    }

    static  getDefaultResponseHandlerSuccess(res, errorCallback) {
        return {
            onSuccess: function (data, message, code) {
                ResponseManager.respondWithSuccess(res, code || ResponseManager.HTTP_STATUS.OK, data, message);
            },
            onError: function (error) {
                errorCallback(error);
            }
        };
    }

    static generateHATEOASLink(link, method, rel) {
        return {
            link: link,
            method: method,
            rel: rel
        }
    }

    static respondWithSuccess(res, code, data, message = "", links = []) {
        let response = Object.assign({}, BasicResponse);
        response.success = true;
        response.message = message;
        response.data = data;
        response.links = links;
        res.status(code).json(response);
    }

    static respondWithErrorData(res, errorCode, message = "", data = "", links = []) {
        let response = Object.assign({}, BasicResponse);
        response.success = false;
        response.message = message;
        response.data = data;
        response.links = links;
        res.status(errorCode).json(response);
    }

    static respondWithError(res, errorCode, message = "", links = []) {
        let response = Object.assign({}, BasicResponse);
        response.success = false;
        response.message = message;
        response.links = links;
        res.status(errorCode).json(response);
    }

}
module.exports = ResponseManager;

There are a bunch of method defined here, also have a look at the format of the response returned to a client.

1
2
3
4
5
const BasicResponse = {
    "success": false,
    "message": "",
    "data": {}
};

This format is not the best choice and quite controversial, but sometimes it useful to have uniform responses.

The User Controller

Now we will define the UserController class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const BaseController = require(APP_CONTROLLER_PATH + 'base');
const UserHandler = require(APP_HANDLER_PATH + 'user');

const util = require("util");

class UserController extends BaseController {
    constructor() {
        super();
        this._userHandler = new UserHandler();
    }

    get(req, res) {
        this._userHandler.getUserInfo(req, this._responseManager.getDefaultResponseHandler(res));
    }

    create(req, res) {
        this._userHandler.createNewUser(req, this._responseManager.getDefaultResponseHandler(res));

    }

    authenticate(req, res, callback) {

    }
}

module.exports = UserController;

We will left the authenticate method without implementation for now and return to it back a little bit later.

The Post Controller

Another controller we need is PostController.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const BaseController = require(APP_CONTROLLER_PATH + 'base');
const PostHandler = require(APP_HANDLER_PATH + 'post/post');

class PostController extends BaseController {
    constructor() {
        super();
        this._postHandler = new PostHandler();
    }

    getAll(req, res) {
        this._postHandler.getAllPosts(req, this._responseManager.getDefaultResponseHandler(res));
    }

    get(req, res) {
        this._postHandler.getSinglePost(req, this._responseManager.getDefaultResponseHandler(res));
    }

    create(req, res) {
        this._postHandler.createNewPost(req, this._responseManager.getDefaultResponseHandler(res));
    }

    update(req, res) {
        this._postHandler.updatePost(req, this._responseManager.getDefaultResponseHandler(res));
    }

    remove(req, res) {
        this._postHandler.deletePost(req, this._responseManager.getDefaultResponseHandler(res));
    }

    authenticate(req, res, callback) {

    }
}

module.exports = PostController;

We have left the authentication method without implementation as well.

6. Authentication strategies

It’s time to turn our attention to authentication. First and foremost, you may have noticed that anyone can create a new user now. We need to protect these routes somehow. For the sake of clarity, we are going to implement a simple passport strategy that uses shared (symmetric) key. However, it is a really bad idea to follow this approach in production. In addition we will create several more strategies for all our endpoints.

A Passport strategy

Passport is authentication middleware for Node.js. It is very modular and flexible, allowing easily extend it and implement custom modules, strategies and middlewares.

We will start by creating a base strategy class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const BasePassportStrategy = require('passport-strategy');

class BaseAuthStrategy extends BasePassportStrategy {
    constructor() {
        super();
    }

    _initStrategy() {
        throw new Error("Not Implemented");
    }

    authenticate(req) {
        throw new Error("Not Implemented");
    }

    authenticate(req, options) {
        throw new Error("Not Implemented");
    }

    get name() {
        throw new Error("Not Implemented");
    }

    provideOptions() {
        throw new Error("Not Implemented");
    }

    provideSecretKey() {
        throw new Error("Not Implemented");
    }
}

exports = module.exports = BaseAuthStrategy;

We are extending the BasePassportStrategy class (under the hood this is just a function) and adding some common methods, that should every authentication strategy implement.

The SecretKey Strategy

As I have mentioned, this strategy is quite simple and should not be used in real projects. The implementation could be the following.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
const BaseAuthStrategy = require(APP_AUTH_STRATEGY + 'base-auth');
const InvalidPayloadError = require(APP_ERROR_PATH + 'invalid-payload');
const UnauthorizedError = require(APP_ERROR_PATH + 'unauthorized');

class SecretKeyAuthStrategy extends BaseAuthStrategy {
    constructor(options) {
        super();
        this._options = options;
        this._initStrategy();
    }

    static get AUTH_HEADER() {
        return "Authorization";
    }

    _initStrategy() {

    }

    get name() {
        return 'secret-key-auth';
    }

    static _extractKeyFromHeader(req) {
        return req.headers[SecretKeyAuthStrategy.AUTH_HEADER.toLowerCase()];
    }

    _verifyCredentials(key) {
        return key === this.provideSecretKey();
    }

    authenticate(req, callback) {
        let secretKey = SecretKeyAuthStrategy._extractKeyFromHeader(req);
        if (!secretKey) {
            return callback.onFailure(new InvalidPayloadError("No auth key provided"));
        }
        if (this._verifyCredentials(secretKey)) {
            return callback.onVerified();
        } else {
            return callback.onFailure(new UnauthorizedError("Invalid secret key"));
        }
    }

    provideSecretKey() {
        return this._options.secretKey;
    }

    provideOptions() {
        return this._options;
    }
}

module.exports = SecretKeyAuthStrategy;

Here’s a quick rundown:

  • the name getter method returns a unique name for the strategy. You will see later why do we need it.
  • the _verifyCredentials method just compares the secret key provided by a user with the provided in options passed to the constructor. The secret key is not hardcoded to give an ability to provide a custom key while configuring the strategy.
  • the authenticate method is called to authenticate a request.

The Credentials Strategy

Another strategy we need to implement, that will accept a login/password pair and verifies whether such a user with the provided credentials exists. This strategy is required to issue a JWT token.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const LocalAuthStrategy = require('passport-local').Strategy;
const UserModel = require(APP_MODEL_PATH + 'user').UserModel;
const UnauthorizedError = require(APP_ERROR_PATH + 'unauthorized');
const NotFoundError = require(APP_ERROR_PATH + 'not-found');

class CredentialsAuthStrategy extends LocalAuthStrategy {
    constructor() {
        super(CredentialsAuthStrategy.provideOptions(), CredentialsAuthStrategy.handleUserAuth);
    }

    get name() {
        return 'credentials-auth';
    }

    static handleUserAuth(username, password, done) {
        UserModel.findOne({email: username}, function (err, user) {
            if (err) {
                return done(err);
            }
            if (!user) {
                return done(new NotFoundError("User not found"), false);
            }
            if (!user.checkPassword(password)) {
                return done(new UnauthorizedError("Invalid credentials"), false);
            }
            return done(null, user);
        });
    }

    static provideOptions() {
        return {
            usernameField: 'email',
            passReqToCallback: false,
            passwordField: 'password',
            session: false
        };
    }

    getSecretKey() {
        throw new Error("No key is required for this type of auth");
    }
}
exports = module.exports = CredentialsAuthStrategy;

Have you noticed that this class extends LocalAuthStrategy. This strategy is provided by the passport-local library. You can check the implementation it is really simple and easy to grasp.

The JWT Strategy

Finally, we need to implement the JWT strategy itself, that will be as an authentication mechanism for most of our endpoints. To read more about JWT please refer to the previous part of the series. We are going to use the RS256 algorithm to sign tokens.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
const passport = require('passport-strategy')
    , jwt = require('jsonwebtoken');

const BaseAuthStrategy = require(APP_AUTH_STRATEGY + 'base-auth');

class JwtRsStrategy extends BaseAuthStrategy {
    constructor(options, verify) {
        super();
        this._options = options;
        this._customVerifier = verify;
        this._initStrategy();
    }

    _initStrategy() {
        passport.Strategy.call(this);
        let options = this.provideOptions();

        if (!options) {
            throw new TypeError('JwtRsStrategy requires options');
        }
        this._privateKey = options.privateKey;
        if (!this._privateKey) {
            throw new TypeError('JwtRsStrategy requires a private key');
        }
        this._publicKey = options.publicKey;
        if (!this._publicKey) {
            throw new TypeError('JwtRsStrategy requires a public key');
        }

        this._extractJwtToken = options.extractJwtToken;
        if (!this._extractJwtToken) {
            throw new TypeError('JwtRsStrategy requires a function to parse jwt from requests');
        }
        this._verifyOpts = {};

        if (options.issuer) {
            this._verifyOpts.issuer = options.issuer;
        }

        if (options.audience) {
            this._verifyOpts.audience = options.audience;
        }

        if (options.algorithms) {
            this._verifyOpts.algorithms = options.algorithms;
        }

        if (options.ignoreExpiration != null) {
            this._verifyOpts.ignoreExpiration = options.ignoreExpiration;
        }
    }

    get name() {
        return 'jwt-rs-auth';
    }

    provideSecretKey() {
        return this._privateKey;
    }

    authenticate(req, callback) {
        let self = this;

        let token = self._extractJwtToken(req);

        if (!token) {
            return callback.onFailure(new Error("No auth token provided"));
        }
        // Verify the JWT
        JwtRsStrategy._verifyDefault(token, this._publicKey, this._verifyOpts, function (jwt_err, payload) {
            if (jwt_err) {
                return callback.onFailure(jwt_err);
            } else {
                try {
                    // If custom verifier was set delegate the flow control
                    if (self._customVerifier) {
                        self._customVerifier(token, payload, callback);
                    }
                    else {
                        callback.onVerified(token, payload);
                    }
                } catch (ex) {
                    callback.onFailure(ex);
                }
            }
        });
    }


    provideOptions() {
        return this._options;
    }

    static _verifyDefault(token, publicKey, options, callback) {
        return jwt.verify(token, publicKey, options, callback);
    }
}

module.exports = JwtRsStrategy;

The most important points of this strategy:

  • jwt = require(‘jsonwebtoken’) we are including the library that provides all necessary functionality for creating, verifying, reading JWT tokens.
  • the _initStrategy method reads and verifies provided options.
  • the authenticate method in it’s turn invokes the jwt.verify implemented by the jsonwebtoken library to verify the token.
  • you can also provide a custom verifier (_customVerifier) that will be called in case of success JWT authentication to perform additional checks.

Initializing strategies

Now having our auth strategies defined, we need to initialize the strategies before using them. We haven’t hardcoded any secret keys in the strategies, therefore we need explicitly provide required options. To do all this configuration stuff, I will use a single class. Do not follow this way in real projects, this is an antipattern, we are mixing different responsibilities and concerns in one single class.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
const BaseAutoBindedClass = require(APP_BASE_PACKAGE_PATH + 'base-autobind');
const JwtRsStrategy = require(APP_AUTH_STRATEGY + 'jwt-rs');
const SecretKeyAuth = require(APP_AUTH_STRATEGY + 'secret-key');
const CredentialsAuth = require(APP_AUTH_STRATEGY + 'credentials');
const ExtractJwt = require("passport-jwt").ExtractJwt;
const JwtToken = require(APP_MODEL_PATH + 'auth/jwt-token');
const RevokedToken = require(APP_MODEL_PATH + 'auth/revoked-token').RevokedTokenModel;
const ForbiddenError = require(APP_ERROR_PATH + 'forbidden');

class AuthManager extends BaseAutoBindedClass {
    constructor() {
        super();
        this._passport = require('passport');
        this._strategies = [];
        this._jwtTokenHandler = require('jsonwebtoken');
        this._setupStrategies();
        this._setPassportStrategies();
    }

    _setupStrategies() {
        // Init JWT strategy
        let jwtOptions = this._provideJwtOptions();
        let secretKeyAuth = new SecretKeyAuth({secretKey: this._provideSecretKey()});
        let jwtRs = new JwtRsStrategy(jwtOptions, this._verifyRevokedToken);
        this._strategies.push(jwtRs);
        this._strategies.push(new CredentialsAuth());
        this._strategies.push(secretKeyAuth);
    }

    _verifyRevokedToken(token, payload, callback) {
        RevokedToken.find({token: token}, function (err, docs) {
            if (docs.length) {
                callback.onFailure(new ForbiddenError("Token has been revoked"));
            } else {
                callback.onVerified(token, payload);
            }
        });
    }

    extractJwtToken(req) {
        return ExtractJwt.fromAuthHeader()(req);
    }

    _provideJwtOptions() {
        let config = global.config;
        let jwtOptions = {};
        jwtOptions.extractJwtToken = ExtractJwt.fromAuthHeader();
        jwtOptions.privateKey = this._provideJwtPrivateKey();
        jwtOptions.publicKey = this._provideJwtPublicKey();
        jwtOptions.issuer = config.jwtOptions.issuer;
        jwtOptions.audience = config.jwtOptions.audience;
        return jwtOptions;
    }

    _provideJwtPublicKey() {
        const fs = require('fs');
        return fs.readFileSync(CONFIG_BASE_PATH + 'secret/jwt-key.pub', 'utf8');
    }

    _provideJwtPrivateKey() {
        const fs = require('fs');
        return fs.readFileSync(CONFIG_BASE_PATH + 'secret/jwt-key.pem', 'utf8');
    }

    _provideSecretKey() {
        const fs = require('fs');
        return fs.readFileSync(CONFIG_BASE_PATH + 'secret/secret.key', 'utf8');
    }

    providePassport() {
        return this._passport;
    }

    getSecretKeyForStrategy(name) {
        for (let i = 0; i < this._strategies.length; i++) {
            let strategy = this._strategies[i];
            if (strategy && strategy.name === name) {
                return strategy.provideSecretKey();
            }
        }
    }

    _setPassportStrategies() {
        let passport = this._passport;
        this._strategies.forEach(function (strategy) {
            passport.use(strategy);
        });
    }

    signToken(strategyName, payload, options) {
        let key = this.getSecretKeyForStrategy(strategyName);
        switch (strategyName) {
            case 'jwt-rs-auth':
                return new JwtToken(
                    this._jwtTokenHandler.sign(payload,
                        key,
                        options)
                );
            default:
                throw new TypeError("Cannot sign toke for the " + strategyName + " strategy");
        }
    }
}
exports = module.exports = new AuthManager();

But again, remember by using a revoked token list we are making our API stateful, so please keep this in mind while making decisions. Good thing about NoSQL databases that they are scalable, therefore there should no problems with synchronization between nodes. Think twice anyway.

7. Applying authentication strategies

Finally, we are ready to apply created strategies to protect our routes. You can also set an authentication strategy as a middleware directly when defining a route, like this.

1
2
3
app.get("/protected_resource", passport.authenticate('jwt', { session: false }), function(req, res){
  res.json("Access allowed");
});

But we will authenticate requests in controllers to keep routes as clean as possible.

In order to use passport add the following lines to the main index.js file, telling the app to use the passport middleware.

1
2
const authManager = require(APP_HANDLER_PATH + 'auth');
app.use(authManager.providePassport().initialize());

Please notice the way we are including the AuthManager. We are not creating an instance, because it is a kind of Singleton.

1
exports = module.exports = new AuthManager();

Securing the User endpoint

The first endpoint we need to secure is the user route. Go to the UserController and modify it as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
const BaseController = require(APP_CONTROLLER_PATH + 'base');
const UserHandler = require(APP_HANDLER_PATH + 'user');

const util = require("util");

class UserController extends BaseController {
    constructor() {
        super();
        this._authHandler = new UserHandler();
        this._passport = require('passport');
    }

    get(req, res, next) {
        let responseManager = this._responseManager;
        let that = this;
        this._passport.authenticate('jwt-rs-auth', {
            onVerified: function (token, user) {
                that._authHandler.getUserInfo(req, user, responseManager.getDefaultResponseHandler(res));
            },
            onFailure: function (error) {
                responseManager.respondWithError(res, error.status || 401, error.message);
            }
        })(req, res, next);
    }

    create(req, res) {
        let responseManager = this._responseManager;
        this.authenticate(req, res, () => {
            this._authHandler.createNewUser(req, responseManager.getDefaultResponseHandler(res));
        });
    }

    authenticate(req, res, callback) {
        let responseManager = this._responseManager;
        this._passport.authenticate('secret-key-auth', {
            onVerified: callback,
            onFailure: function (error) {
                responseManager.respondWithError(res, error.status || 401, error.message);
            }
        })(req, res);
    }

}

module.exports = UserController;

Now you can test these endpoints. For testing any API I am using a great application for ChromePostman. It is an awesome application for debugging HTTP requests, in addition it is free.

It’s important to note that we are using different authentication strategies for different routes. Owing to the specificity of the user info route we need to authorize a request using the jwt-rs-auth strategy. However this route is not mandatory.

Here is an example of request for creating a user. Do not forget to include the Authorization header with the secret key.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
POST /v1/user/ HTTP/1.1
Host: localhost:3000
Content-Type: application/json
Authorization: supersecretbulletproofkey
Cache-Control: no-cache
Postman-Token: 1dff2ab2-1135-6b41-9bed-a2ae239ffc2d
{
  "firstName": "Alexander",
  "lastName" : "Molochko",
  "email" : "[email protected]",
  "password" : "superstrongpassword"
}

The Auth Route

The Auth route has only two endpoints defined.

1
2
3
4
5
6
7
8
const router = require('express').Router();
const AuthController = require(APP_CONTROLLER_PATH + 'auth');
let authController = new AuthController();

router.post('/', authController.create);
router.delete('/:token', authController.remove);

module.exports = router;

The AuthHandler

When we have the user created we can now use credentials to get a JWT token. But we haven’t implemented the logic to issue the token. Let’s do this now. Create the AuthHandler class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
const RevokedToken = require(APP_MODEL_PATH + 'auth/revoked-token').RevokedTokenModel;
const NotFoundError = require(APP_ERROR_PATH + 'invalid-payload');
const BaseAutoBindedClass = require(APP_BASE_PACKAGE_PATH + 'base-autobind');
let crypto = require('crypto');
const SHA_HASH_LENGTH = 64;
const ForbiddenError = require(APP_ERROR_PATH + 'forbidden');

class AuthHandler extends BaseAutoBindedClass {
    constructor() {
        super();
        this._jwtTokenHandler = require('jsonwebtoken');
        this._authManager = require(APP_MANAGER_PATH + 'auth');
    }

    issueNewToken(req, user, callback) {
        let that = this;
        if (user) {
            let userToken = that._authManager.signToken("jwt-rs-auth", that._provideTokenPayload(user), that._provideTokenOptions());
            callback.onSuccess(userToken);
        }
        else {
            callback.onError(new NotFoundError("User not found"));
        }
    }

    revokeToken(req,token, callback) {
        let that = this;
        req.checkParams('token', 'Invalid token id provided').notEmpty().isAlphanumeric().isLength(SHA_HASH_LENGTH);
        req.getValidationResult()
            .then((result) => {
                if (!result.isEmpty()) {
                    let errorMessages = result.array().map(function (elem) {
                        return elem.msg;
                    });
                    throw new ForbiddenError('Invalid token id :' + errorMessages.join(' && '));
                }
                let tokenHashedId = req.params.token;
                if (that.checkIfHashedTokenMatches(token, tokenHashedId)) {
                    return new RevokedToken({token: token});
                }
                else {
                    throw new ForbiddenError('Invalid credentials');
                }
            })
            .then((token) => {
                token.save();
                return token;
            })
            .then((token) => {
                callback.onSuccess("Token has been successfully revoked");
            })
            .catch((error) => {
                callback.onError(error);
            });
    }

    _hashToken(token) {
        return crypto.createHash('sha256').update(token).digest('hex');
    }

    checkIfHashedTokenMatches(token, hashed) {
        let hashedValid = this._hashToken(token);
        return hashedValid === hashed;
    }


    _provideTokenPayload(user) {
        return {
            id: user.id,
            scope: 'default'
        };
    }

    _provideTokenOptions() {
        let config = global.config;
        return {
            expiresIn: "10 days",
            audience: config.jwtOptions.audience,
            issuer: config.jwtOptions.issuer,
            algorithm: config.jwtOptions.algorithm
        };
    }
}
module.exports = AuthHandler;

What we’ve done in the code above ?

  • the issueNewToken method checks whether a user with provided credentials was found and issues a new JWT token. Please also note that authentication is done in the controller as we will see in a while.
  • the _provideTokenOptions method provides options for the JWT token.
  • the _provideTokenPayload method returns the object that will the payload part in the JWT token.

Also I’ve defined the revokeToken method. This method is just for testing token revocation. In order to make this route more RESTFul we are passing a SHA256 hashed token as the parameter for the auth endpoint, like that http://localhost:3000/v1/auth/4005a6ae7c545d6eb1ba22c42e2f11a3637bbc7c1c496cba5ff12d924414d56a

This makes it a little more explicit and reduces the token size, because if there is no parameter at all it is not clear what we are trying to delete. However, the common practice for revoking tokens is to send the POST request to a similar endpoint /oauth/revoke. But for this tutorial we will use the DELETE method.

The AuthController

The last part we need to implement is the AuthController class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
const BaseController = require(APP_CONTROLLER_PATH + 'base');
const AuthHandler = require(APP_HANDLER_PATH + 'auth');

class AuthController extends BaseController {
    constructor() {
        super();
        this._authHandler = new AuthHandler();
        this._passport = require('passport');
    }

    // Request token by credentials
    create(req, res, next) {
        let responseManager = this._responseManager;
        let that = this;
        this.authenticate(req, res, next, (user) => {
            that._authHandler.issueNewToken(req, user, responseManager.getDefaultResponseHandler(res));
        });
    }

    // Revoke Token
    remove(req, res, next) {
        let responseManager = this._responseManager;
        let that = this;
        this._passport.authenticate('jwt-rs-auth', {
            onVerified: function (token, user) {
                that._authHandler.revokeToken(req, token, responseManager.getDefaultResponseHandler(res));
            },
            onFailure: function (error) {
                responseManager.respondWithError(res, error.status || 401, error.message);
            }
        })(req, res, next);

    }

    authenticate(req, res, next, callback) {
        let responseManager = this._responseManager;
        this._passport.authenticate('credentials-auth', function (err, user) {
            if (err) {
                responseManager.respondWithError(res, err.status || 401, err.message || "");
            } else {
                callback(user);
            }
        })(req, res, next);
    }

}

module.exports = AuthController;

We are also using different authentication strategies in this controller.

Now let’s get our first JWT token by sending the following request.

1
2
3
4
5
6
7
8
9
POST /v1/auth/ HTTP/1.1
Host: localhost:3000
Content-Type: application/json
Cache-Control: no-cache
Postman-Token: d0957c0b-fb53-3bd4-6290-a5b141eb42d9
{
  "email" : "[email protected]",
  "password" : "superstrongpassword"
}

You should get the similar response.

1
2
3
4
5
6
7
8
{
  "success": true,
  "message": "",
  "data": {
    "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU5M2ZhZjUwMGE4ZjNiMWE5YTBjN2Q3MiIsInNjb3BlIjoiZGVmYXVsdCIsImlhdCI6MTQ5NzM3NjU0MSwiZXhwIjoxNDk4MjQwNTQxLCJhdWQiOiJhcGkuYmxvZy5jcm9zcC5uZXQiLCJpc3MiOiJhcGlAY3Jvc3AubmV0In0.GTQL-CpVViSb6hS9rHHWNh46jy5h1ylbSkSRZJypE7pPhjjq4I7WTi0OXE_vpLYLJJ96GWRoerwafnfnvANeko3hLHJkb_V6c1ujxi5Luxlu3y8cDfCCN00CKdonv_aiJ45BFHRAKco4F2dTci2STXSDTGBFQLUUXcBBD2wlLMw"
  },
  "links": []
}

Save it somewhere and be ready to use it for accessing protected routes.

Securing routes with JWT

Now we need to protect our blog post routes with the JWT authentication. Modify the PostController in the following way.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
const BaseController = require(APP_CONTROLLER_PATH + 'base');
const PostHandler = require(APP_HANDLER_PATH + 'post');

class PostController extends BaseController {
    constructor() {
        super();
        this._postHandler = new PostHandler();
        this._passport = require('passport');
    }

    getAll(req, res, next) {
        this.authenticate(req, res, next, (token, user) => {
            this._postHandler.getAllPosts(req, this._responseManager.getDefaultResponseHandler(res));
        });
    }

    get(req, res, next) {
        this.authenticate(req, res, next, (token, user) => {
            this._postHandler.getSinglePost(req, this._responseManager.getDefaultResponseHandler(res));
        });
    }

    create(req, res, next) {
        this.authenticate(req, res, next, (token, user) => {
            this._postHandler.createNewPost(req, this._responseManager.getDefaultResponseHandler(res));
        });
    }

    update(req, res, next) {
        this.authenticate(req, res, next, (token, user) => {
            this._postHandler.updatePost(req, this._responseManager.getDefaultResponseHandler(res));
        });
    }

    remove(req, res, next) {
        this.authenticate(req, res, next, (token, user) => {
            this._postHandler.deletePost(req, this._responseManager.getDefaultResponseHandler(res));
        });
    }

    authenticate(req, res, next, callback) {
        let responseManager = this._responseManager;
        this._passport.authenticate('jwt-rs-auth', {
            onVerified: callback,
            onFailure: function (error) {
                responseManager.respondWithError(res, error.status || 401, error.message);
            }
        })(req, res, next);
    }
}

module.exports = PostController;

8. Testing API

It is time to test our API. Here is an example of the request to get all posts.

1
2
3
4
5
6
GET /v1/posts HTTP/1.1
Host: localhost:3000
Content-Type: application/json
Authorization: JWT eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU5M2ZhZjUwMGE4ZjNiMWE5YTBjN2Q3MiIsInNjb3BlIjoiZGVmYXVsdCIsImlhdCI6MTQ5NzM3NjU0MSwiZXhwIjoxNDk4MjQwNTQxLCJhdWQiOiJhcGkuYmxvZy5jcm9zcC5uZXQiLCJpc3MiOiJhcGlAY3Jvc3AubmV0In0.GTQL-CpVViSb6hS9rHHWNh46jy5h1ylbSkSRZJypE7pPhjjq4I7WTi0OXE_vpLYLJJ96GWRoerwafnfnvANeko3hLHJkb_V6c1ujxi5Luxlu3y8cDfCCN00CKdonv_aiJ45BFHRAKco4F2dTci2STXSDTGBFQLUUXcBBD2wlLMw
Cache-Control: no-cache
Postman-Token: 11fa4304-9251-5aee-2fd8-471880f0f99b

Please note that the JWT token format is the following (ExtractJWT library format).

1
2
JWT <token>
JWT eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU5M2ZhZjUwMGE4ZjNiMWE5YTBjN2Q3MiIsInNjb3BlIjoiZGVmYXVsdCIsImlhdCI6MTQ5NzM3NjU0MSwiZXhwIjoxNDk4MjQwNTQxLCJhdWQiOiJhcGkuYmxvZy5jcm9zcC5uZXQiLCJpc3MiOiJhcGlAY3Jvc3AubmV0In0.GTQL-CpVViSb6hS9rHHWNh46jy5h1ylbSkSRZJypE7pPhjjq4I7WTi0OXE_vpLYLJJ96GWRoerwafnfnvANeko3hLHJkb_V6c1ujxi5Luxlu3y8cDfCCN00CKdonv_aiJ45BFHRAKco4F2dTci2STXSDTGBFQLUUXcBBD2wlLMw

Test other routes in a very similar way, they should work fine as well. Moreover, here is the link to all requests that are used throughout this article, you could import them into Postman.

Try to modify the token and make a request. Here is the response you will probably get.

1
2
3
4
5
6
{
  "success": false,
  "message": "invalid signature",
  "data": {},
  "links": []
}

9. Adding HATEOAS links

As you you can see from responses, there is an empty array returned all the time called links. It’s purpose is to contain HATEOAS links that enables a client to navigate between different endpoints. Let’s add one link for the single post route that will point to a collection of all available posts. Modify the get method of the PostController class as follows.

1
2
3
4
5
6
7
8
9
    get(req, res, next) {
        let responseManager = this._responseManager;
        this.authenticate(req, res, next, (token, user) => {
            this._postHandler.getSinglePost(req, responseManager.getDefaultResponseHandlerError(res, ((data, message, code) => {
                let hateosLinks = [responseManager.generateHATEOASLink(req.baseUrl, "GET", "collection")];
                responseManager.respondWithSuccess(res, code || responseManager.HTTP_STATUS.OK, data, message, hateosLinks);
            })));
        });
    }

We are providing the default response handler for errors, but handle successful callback manually, by adding the link to a collection of all posts.

Now try to make a request to the endpoint /v1/posts/5940296af75b642036c7484a (replace the id with a existing one) and you should get the similar result.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
{
  "success": true,
  "message": "",
  "data": {
    "_id": "5940296af75b642036c7484a",
    "title": "Android Clean Architecture : Part 1 ? Introduction",
    "content": "In this post series I want to share my experience dealing with the concept of Clean Architecture with the Android background. This topic is still highly discussed, but was proposed quite long time ago. What do you think about at first when you start a new software project ? Do you think about a data store you are going to use ? Maybe, a great cloud of modern frameworks appears in you head and you can?t wait to start using them all ?",
    "authorId": "593e7bd2bece93108570089c",
    "dateModified": "2017-06-13T18:05:30.980Z",
    "dateCreated": "2017-06-13T18:05:30.973Z"
  },
  "links": [
    {
      "link": "/v1/posts",
      "method": "GET",
      "rel": "collection"
    }
  ]
}

Conclusion

The main purpose of this tutorial is to show how you can combine different technologies to build a secure REST API. I hope that now you have a better understanding of how to create a REST API using NodeJS. The created project is not the best possible way of implementing REST API, it is intended to show just a possible example. There are a lot of thing that can be improved and refactored. Also please do not forget to document your API, or use existing tools like Swagger to generate documentation for an API. If you have any troubles with the described steps, please feel free to leave comments below.

Source code
This article is the part 2 of 2 of the series Creating a Secure REST API using NodeJS