Securing Node.js RESTful APIs with JSON Web Tokens(JWT) using Async-await
Have you ever wondered how authentication works? What’s behind all the complexity and abstractions.Actually, nothing special.It’s a way of encrypting a value, in turn creating a unique token that users use as an identifier.This token verifies your identity.It can authenticate who you are,and authorize various resources you have access to.
Before I begin, there are some things you need to know about Node.js and some EcmaScript standards I’ll be using.
The most important point --We won’t be using callbacks, We even won’t be using traditional Promises. We are going to use the great async-await feature introduced in NodeJS 7.6.Also, the whole demo is on GitHub
Lets setup the environment
First go here — https://nodejs.org/en/download/ and Install NodeJS.
Download and Install MongoDB — https://www.mongodb.com/download-center#community
Now the environment Setup is done. Let’s get into the command line.
First we need to install ExpressJS. The most popular NodeJS Framework.
npm install -g express express-generator
This will install ExpressJS and the ExpressJS Official generator packages. Now let’s generate the Application using the Express Generator.
express --view=ejs mean
Now the express App is generated. Go inside the directory.
cd mean
npm install
All the necessary packages will be installed.
At first let’s install all the necessary packages we will be using throughout this app.
npm install --save bluebird mongoose mongoose-paginate
Our directory structure should look like as —
Now lets code something!
Database
Its a good habit to maintain a configuration file , create config.js (in the project directory) file and put this line of code we need it later
//config.js
module.exports = {
'SECRET': 'supersecret',
'DATABASE': 'mongodb://127.0.0.1:27017/mean' //You can set whatever your db
};
Disclaimer: Have in mind, under no circumstances should you ever, (EVER!) have your secret key publicly visible like this. Always put all of your keys in environment variables! I’m only writing it like this for demo purposes.
Now start MongoDB.Add mongoose support to the app.js file.
var bluebird = require('bluebird')
var mongoose = require('mongoose')
var config = require('./config')
mongoose.Promise = bluebird
mongoose.connect(config.DATABASE)
.then(() => {
console.log(`Succesfully Connected to the Mongodb Database..`)
})
.catch(() => {
console.log(`Error Connecting to the Mongodb Database...`)
})
// catch 404 and forward to error handler
app.use(function (req, res, next) {
next(createError(404));
});
Set the proper CORS headers ,for cross origin domain requests , so that we can create our frontend with angular or react (PART-|| for MEAN/MERN ) and can communicate with our endpoints
app.use(function (req, res, next) {
res.header("Access-Control-Allow-Origin", "http://localhost:4200"); //* will allow from all cross domain
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
next();
});
Lets create api directory in routes folder and create user.route.js file and put the following codes
// /routes/api/user.route.js
var express = require('express')
var router = express.Router()
var UserController = require('../../controllers/users.controller');
var Authorization = require('../../auth/authorization');
// Authorize each API with middleware and map to the Controller Functions
router.post('/registration', UserController.createUser)
router.post('/login/', UserController.loginUser)
router.get('/', Authorization, UserController.getUsers)
router.delete('/:id', Authorization, UserController.removeUser)
// Export the Router
module.exports = router;
Now we set all endpoints with the middlware Authorization to authenticate our APIs to route this APIs create api.js file in routes and put the following codes
// routes/api.js
var express = require('express')
var router = express.Router()
var users = require('./api/user.route')
router.use('/users', users);
module.exports = router;
Create controller directory to write our logic for the endpoints,create a file users.controller.js and put some code
var UserService = require('../services/user.service');
exports.createUser = async function (req, res, next) {
// Req.Body contains the form submit values.
var User = {
name: req.body.name,
email: req.body.email,
password: req.body.password
}
try {
// Calling the Service function with the new object from the Request Body
var createdUser = await UserService.createUser(User)
return res.status(201).json({data: createdUser, message: "Succesfully Created User"})
} catch (e) {
//Return an Error Response Message with Code and the Error Message.
return res.status(400).json({status: 400, message: "User Creation was Unsuccesfull"})
}
}
exports.removeUser = async function (req, res, next) {
var id = req.params.id;
try {
var deleted = await UserService.deleteUser(id);
res.status(200).send("Succesfully User Deleted");
} catch (e) {
return res.status(400).json({status: 400, message: e.message})
}
}
exports.loginUser = async function (req, res, next) {
// Req.Body contains the form submit values.
var User = {
email: req.body.email,
password: req.body.password
}
try {
// Calling the Service function with the new object from the Request Body
var loginUser = await UserService.loginUser(User);
return res.status(201).json({data: loginUser, message: "Succesfully login"})
} catch (e) {
//Return an Error Response Message with Code and the Error Message.
return res.status(400).json({status: 400, message: "Invalid username or password"})
}
}
exports.getUsers = async function (req, res, next) {
// Check the existence of the query parameters, If doesn't exists assign a default value
var page = req.query.page ? req.query.page : 1
var limit = req.query.limit ? req.query.limit : 10;
try {
var Users = await UserService.getUsers({}, page, limit)
// Return the Users list with the appropriate HTTP password Code and Message.
return res.status(200).json({status: 200, data: Users, message: "Succesfully Users Recieved"});
} catch (e) {
//Return an Error Response Message with Code and the Error Message.
return res.status(400).json({status: 400, message: e.message});
}
}
Now we have to create services directory and a file user.services.js, if you are facing any problem in file structure just see the above file directory figure.
Open up a terminal window in your project folder and install the following modules:
npm install jsonwebtoken --save
npm install bcryptjs --save
That’s all the modules we need to implement our desired Authorization.
Put the following lines
//user.services.js
var User = require('../models/User.model');
var config = require('../config');
var bcrypt = require('bcryptjs');
var jwt = require('jsonwebtoken');
exports.createUser = async function (user) {
// Creating a new Mongoose Object by using the new keyword
var hashedPassword = bcrypt.hashSync(user.password, 8);
var newUser = new User({
name: user.name,
email: user.email,
date: new Date(),
password: hashedPassword
})
try {
// Saving the User
var savedUser = await newUser.save();
var token = jwt.sign({id: savedUser._id}, config.SECRET, {
expiresIn: 86400 // expires in 24 hours
});
return token;
} catch (e) {
// return a Error message describing the reason
throw Error("Error while Creating User")
}
}
exports.loginUser = async function (user) {
// Creating a new Mongoose Object by using the new keyword
try {
// Find the User
var _details = await User.findOne({ email: user.email });
var passwordIsValid = bcrypt.compareSync(user.password, _details.password);
if (!passwordIsValid) throw Error("Invalid username/password")
var token = jwt.sign({id: _details._id}, config.SECRET, {
expiresIn: 86400 // expires in 24 hours
});
return token;
} catch (e) {
// return a Error message describing the reason
throw Error("Error while Login User")
}
}
exports.deleteUser = async function (id) {
// Delete the User
try {
var deleted = await User.remove({_id: id})
if (deleted.n === 0 && deleted.ok === 1) {
throw Error("User Could not be deleted")
}
return deleted;
} catch (e) {
throw Error("Error Occured while Deleting the User")
}
}
// Async function to get the User List
exports.getUsers = async function (query, page, limit) {
// Options setup for the mongoose paginate
var options = {
page,
limit
}
// Try Catch the awaited promise to handle the error
try {
var Users = await User.paginate(query, options)
// Return the Userd list that was retured by the mongoose promise
return Users;
} catch (e) {
// return a Error message describing the reason
throw Error('Error while Paginating Users');
}
}
we are going to set JSON Web Token (JWT) at the time of user registration and login and attched token in response with the expiration time so that for requesting the resource, token should be in headers with the name x-acees-token
to authorize the endpoints
For Registration
Here we’re expecting the user to send us three values, a name, an email and a password. We’re immediately going to take the password and encrypt it with Bcrypt’s hashing method. Then take the hashed password, include name and email and create a new user. After the user has been successfully created, we’re at ease to create a token for that user.
The jwt.sign() method takes a payload and the secret key defined in config.js as parameters.It creates a unique string of characters representing the payload. In our case, the payload is an object containing only the id of the user.
For Login
First of all we check if the user exists. Then using Bcrypt’s .compareSync() method we compare the password sent with the request to the password in the database. If they match we .sign() a token.
Modals
Create models directory and a file user.model.js inside modals for database schema and put the following codes
var mongoose = require('mongoose')
var mongoosePaginate = require('mongoose-paginate')
var UserSchema = new mongoose.Schema({
name: String,
email: String,
password: String,
date: Date
})
UserSchema.plugin(mongoosePaginate)
const User = mongoose.model('Users', UserSchema)
module.exports = User;
Endpoint Authorization
JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties.The claims in a JWT are encoded as a JSON object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or integrity protected with a Message Authentication Code (MAC) and/or encrypted.
No intruders will allow, We’re going to use middleware function to check if a token exists and whether it is valid.After validating it, we add the decoded.id value to the request (req) variable.We now have access to it in the next function in line in the request-response cycle. Calling next() will make sure flow will continue to the next function waiting in line. In the end, we export the function.
Middleware functions are functions that have access to the request object (req), the response object (res),and the next function in the application’s request-response cycle. The next function is a function in the Express router which, when invoked, executes the middleware succeeding the current middleware.
Lets create auth folder in our project directory and create authorization.js file inside auth and put the following logic
var jwt = require('jsonwebtoken');
var config = require('../config');
var authorization = function (req, res, next) {
var token = req.headers['x-access-token'];
var msg = {auth: false, message: 'No token provided.'};
if (!token) res.status(500).send(msg);
jwt.verify(token, config.SECRET, function (err, decoded) {
var msg = {auth: false, message: 'Failed to authenticate token.'};
if (err) res.status(500).send(msg);
next();
});
}
module.exports = authorization;
Here we’re expecting the token be sent along with the request in the headers.The default name for a token in the headers of an HTTP request is x-access-token.If there is no token provided with the request the server sends back an error.To be more precise, an 401 unauthorized status with a response message of ‘No token provided’.If the token exists, the jwt.verify() method will be called. This method decodes the token making it possible to view the original payload. We’ll handle errors if there are any and if there are not, send back the decoded value as the response.
Let’s test this...
Just run npm start in your terminal and open up your REST API testing tool of choice, I use Postman or Insomnia, but any will do.Go back to your terminal and run node server.js. If it is running, stop it, save all changes to you files, and run node server.js again.
Open up Postman and hit the register endpoint (http://localhost:3000/api/users/registration). Make sure to pick the POST method and x-www-form-url-encoded.Now, add some values. My user’s name is 'Mohammad' , email is 'mohdabdur786@gmail.com' and his password is 'Wow@123'
See the response? The token is a long jumbled string.
Now first copy the token. Change the URL to http://localhost:3000/api/users , and the method to GET and add the token to the request header with name x-access-token
You will get list of users...
Try to update users the http://l
ocalhost:3000/api/users endpoint, and the method to PUT with x-www-form-url-encoded.Now change the name 'Abdur' , email is 'mohdabdur786@gmail.com' and his password is 'Wow@123'
Delete some users hit http://localhost:3000/api/users/(_id) endpoint with the method DELETE.
Cool all works! What if we get the wrong token?
Great, when the token is wrong the server sends a response status of 401 unauthorized. Just what we wanted!
Feel free to mess with the token and try the endpoint again. With an invalid token, you’ll see the desired error message, and be sure the code you wrote works the way you want.
Hope you guys enjoyed reading this as much as I enjoyed writing it. Until next time, be curious and have fun.
You can clone from GitHub
Hi, can you explain me who you start the server. The server.js is part of this project? or you have an independent process running. Thanks.
sorry, how you start the server.
Could you provide a code of use environment variables? (best practice)
Disclaimer: Have in mind, under no circumstances should you ever, (EVER!) have your secret key publicly visible like this. Always put all of your keys in environment variables! -
Thanks for comment Yuriy…Definitely its a good practice to use environment variables but it just a dummy data to make it easier to understand the code flow.
Can you show how to correctly load and use environment variables? And the second question is do you have a tutorial project which uses the current Securing Node.js RESTful APIs? I learned a lot from your good example but it is not enough because is required an example of Front-End. Then It will be the complete solution for teaching a full-stack. Thank you a lot for your current post.
I have updated the repo on GitHub where the constant variable
loaded from .env file and I will post front-end part also.
Thanks Yuriy.
Your example is great.
If you have able add functionalities described below then your project will a complete course for beginners - How to properly develop a project in NodeJS.
List of additional functionality on API and Front End for the existing project.
3.1) Add one or many users photos when adding a new user
3.2) Add/Replace/Remove photos when editing an existing user
3.3) Remove photos when deleting a user.