How to Secure REST API with JWT
By: Sunil Kumar
Tags: Restful, Nodejs, Rest Api, Restful Api, Jwt

In this tutorial you will learn how to create, secure REST API using JWT (JSON Web Token), Nodejs, Express, Mongoose, and MongoDB Atlas cloud database.

Please checkout the full video tutorial walk through for this tutorial. Download the complete working source code.

Step 1: You need the latest nodejs installed from nodejs.org, we create a new node js app, with all defaults, using below command,

npm init

Step 2: We install all required node packages with npm (node package manager)

npm install bcrypt //For hashing your password before saving into the database.

npm install cors //Allows restricted resources on a web page to be requested from another domain.

npm install dotenv //Storing configuration in the environment separate from code.

npm install express //Web application server framework, designed for building single-page web applications, APIs.

npm install jsonwebtoken //Compact, self-contained way for securely transmitting information between parties as a JSON object.

npm install mongoose //Schema-based solution to model your application data. Object Data Modeling (ODM) library for MongoDB.

npm install nodemon //Utility that will monitor for any changes in your source and automatically restart your server.

We change the node app entry point default (index.js) file in the package.json to main.js, if you did not change at the time of npm init, as below,

{

"name": "restdemo",

"version": "1.0.0",

"description": "secure api demo with jwt",

"main": "main.js", //<--- node app entry point default (index.js)

"scripts": {

"test": "echo \"Error: no test specified\" && exit 1",

},

"author": "Sunil Kumar",

"license": "ISC",

"dependencies": {

"bcrypt": "^4.0.1",

"cors": "^2.8.5",

"dotenv": "^8.2.0",

"express": "^4.17.1",

"jsonwebtoken": "^8.5.1",

"mongoose": "^5.9.18",

"nodemon": "^2.0.4"

}

}

We also setup nodemon to auto reload and listen for changes as below,

"scripts": {

...

...

"start": "nodemon server.js"

}

Step 3: We create two files in project root server.js and main.js as below

//main.js const express = require('express');

const app = express(); module.exports = app;

Before we create server.js we add a new .env file in the project root as below,

//.env PORT = 2019 //server.js const http = require('http');

const app = require('./main');

const port = process.env.PORT || 2021; const server = http.createServer(app); server.listen(port); console.log("server running on:" + port);

Test our node app with below command,

npm start

If all goes well, you should see below in cmd/terminal,

server running on: 2019 or 2021

Step 4: We create api route, model schema, and our auth middleware as below,

We create a new folder named as “api” in our project root, inside we add sub folders/files as below,

/api/routes/user.js

/api/models/user.js

/api/middleware/auth.js

/api/models/userAddress.js

./public.key //<-- move to .env

./private.key //<-- Never share or publish to the outer world // "/api/models/user.js" const mongoose = require('mongoose'); const userSchema = mongoose.Schema({

_id: mongoose.Schema.Types.ObjectId,

firstName: { type: String, required: true },

lastName: { type: String, required: true },

email: {

type: String,

required: true,

unique: true,

match: /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/

},

password: {

type: String,

required: true

},

contact: { type: String },

profilePic: { type: String },

createdAt: Date,

updatedAt: Date,

isAdmin: { type: Boolean }

}); module.exports = mongoose.model('User', userSchema); // "/api/routes/user.js" const express = require('express');

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

const mongoose = require('mongoose');

const jwt = require('jsonwebtoken'); const authenticate = require('../middleware/auth'); const User = require('../models/user');

const UserAddress = require('../models/userAddress'); const fs = require('fs');

var privateKEY = fs.readFileSync('./private.key', 'utf8'); //<-- move to .env for advance security var i = 'Test'; // Issuer //<-- move to .env for advance security

var s = 'some@user.com'; // Subject //<-- move to .env for advance security

var a = 'http://test.in'; // Audience//<-- move to .env for advance security var signOptions = { //<-- move to .env for advance security

issuer: i,

subject: s,

audience: a,

expiresIn: "1h",

algorithm: "RS256"

}; router.post('/signup', (req, res, next) => {

console.log(req.body.email);

User.findOne({email: req.body.email})

.exec()

.then(user => { if(user){

return res.status(500).json({

message: 'Email already in use'

})

}else{ bcrypt.hash(req.body.password, 10, (err, hash) => {

if(err){

return res.status(500).json({

error: 'Something went wrong'

});

}else{

const user = new User({

_id: new mongoose.Types.ObjectId(),

firstName: req.body.firstName,

lastName: req.body.lastName,

email: req.body.email,

password: hash,

createdAt: new Date().toISOString(),

isAdmin: req.body.isAdmin

}); user.save()

.then(doc => {

res.status(201).json({

message: 'Account created successfully'

});

})

.catch(er => {

res.status(500).json({

error: er

});

});

}



}); }

});

}); router.post('/login', async (req, res, next) => {

await User.findOne({email: req.body.email})

.select('_id firstName lastName email password isAdmin')

.exec()

.then(user => {

if(user){ bcrypt.compare(req.body.password, user.password, (err, result) => {

if(err){

return res.status(500).json({

message: 'Login Failed'

})

}else{

if(result){

console.log("user.isAdmin>>>"+user)

const payload = {

userId: user._id,

isAdmin: user.isAdmin

}

jwt.sign(payload, privateKEY, signOptions, (err, token) => {

if(err){

return res.status(500).json({

message: 'Login Failed'

});

}else{

res.status(200).json({

message: {

user: {

userId: user._id,

firstName: user.firstName,

lastName: user.lastName,

email: user.email,

isAdmin: user.isAdmin

},

token: token

}

})

}

})

}else{

res.status(500).json({

message: 'Incorrect username or password'

});

}

}

}); }else{

res.status(500).json({

message: 'Email doesn\'t not exists'

});

}

})

.catch(error => {

res.status(500).json({

error: error

});

})

}); router.post('/new-address', authenticate, (req, res, next) => {

UserAddress.findOne({"user": req.body.userId})

.exec()

.then(user => { if(user){ UserAddress.findOneAndUpdate({"user": req.body.userId}, {

$push: {

"address": req.body.address

}

}, {

new: true

})

.then(doc => {

res.status(201).json({

message: doc

});

}); }else{ const userAddress = new UserAddress({

_id: new mongoose.Types.ObjectId(),

user: req.body.userId,

address: req.body.address

}); userAddress.save()

.then(doc => {

res.status(201).json({

message: doc

});

})

.catch(error => {

res.status(500).json({

error: error

});

}) } });

}); router.get('/get-addresses/:userId', authenticate, (req, res, next) => { console.log(req.params.userId); UserAddress.findOne({"user": req.params.userId})

.select('_id user address')

.exec()

.then(user => {

res.status(200).json({

message: user

})

})

.catch(error => { console.log(error);

res.status(500).json({

error: error

})

}) }); module.exports = router; // "/api/middleware/auth.js" const jwt = require('jsonwebtoken'); const fs = require('fs'); var publicKEY = fs.readFileSync('./public.key', 'utf8'); //<-- move to .env for advance security var i = 'Test'; // Issuer //<-- move to .env for advance security

var s = 'some@user.com'; // Subject //<-- move to .env for advance security

var a = 'http://test.in'; // Audience //<-- move to .env for advance security var verifyOptions = { //<-- move to .env for advance security

issuer: i,

subject: s,

audience: a,

expiresIn: "1h",

algorithm: ["RS256"]

}; const authenticate = (req, res, next) => {

try {

const token = req.headers["x-access-token"] || req.headers["authorization"];



//console.log("token" + token) if (!token) return res.status(401).json({

error: "Sorry! You are not authorised"

}); if(token){ console.log("token>>" + token)

const tokenval = token.split(" ")[1];

const decoded = jwt.verify(tokenval, publicKEY, verifyOptions);

next();

}





} catch (ex) {

//console.log(ex)

res.status(400).json({

error: "Access denied"

});

}

} module.exports = authenticate; // "/api/models/userAddress.js" const mongoose = require('mongoose'); const userAddressSchema = mongoose.Schema({

_id: mongoose.Schema.Types.ObjectId,

user: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },

address: [{

fullName: { type: String, required: true },

mobileNumber: { type: Number, required: true },

pinCode: { type: Number, required: true },

locality: { type: String, required: true },

address: { type: String, required: true },

cityDistrictTown: { type: String, required: true },

state: { type: String, required: true },

landmark: String,

alternatePhoneNumber: Number

}]

}); module.exports = mongoose.model('UserAddress', userAddressSchema); // "./private.key" -----BEGIN RSA PRIVATE KEY----- -----END RSA PRIVATE KEY----- // "./public.key" -----BEGIN RSA PUBLIC KEY----- -----END RSA PUBLIC KEY-----

We update MongoDB Atlas credentials as below, in .env file

//.env MONGODB_USERNAME = ''

MONGODB_PASSWORD = ''

PORT = 2019

This will be used in (main.js) in below code chunk,

mongoose.connect(

'mongodb+srv://'+ process.env.MONGODB_USERNAME +':'+ process.env.MONGODB_PASSWORD +'@xxx-xxx-xx-xx-xx?retryWrites=true&w=majority',

{

useNewUrlParser: true,

useUnifiedTopology: true

},

() => console.log('connection to database success')

);

To setup mongoDB Atlas you need to follow my previous tutorial on Build NodeJS MVP using free MongoDB Atlas cluster for AWS, Google Cloud Platform and Microsoft Azure

Step 5: We use Postman desktop client from https://www.postman.com/ to test run our APIs.

That’s it, now we are ready to consume the APIs from any Web Application or Mobile Application client.

Please checkout the full video tutorial walk through for this tutorial. Download the complete working source code.