Node.js JWT Authentication & MongoDB – Express RestAPIs + JSON Web Token + BCryptjs + Mongoose

nodejs-jwt-authentication-express-bcryptjs-jsonwebtoken-mongoose-feature-image

JSON Web Token defines a compact and self-contained way for securely transmitting information as a JSON object.
In the tutorial, we show how to build a Nodejs Token Authentication RestAPIs with JSON Web Token (JWT) and MongoDB.

Related posts:
Mongoose Many-to-Many related models with NodeJS/Express, MongoDB
Crud RestAPIs with NodeJS/Express, MongoDB using Mongoose

Technologies

– Nodejs/Express
– Json Web Token
– BCryptjs
– Mongoose
– MongoDB

JSON Web Token

JSON Web Token (JWT) defines a compact and self-contained way for securely transmitting information between parties as a JSON object.

-> Scenarios where JSON Web Tokens are useful:

  • Authorization: the most common scenario for using JWT. Single Sign On is a feature that widely uses JWT
  • Information Exchange: Because JWTs can be signed, JSON Web Tokens are a good way of securely transmitting information between parties.

JSON Web Tokens consist of 3 parts:

  • Header
  • Payload
  • Signature

-> JWT looks like Header-Base64-String.Payload-Base64-String.Signature-Base64-String

Header consists of two parts:

  • token type.
  • hashing algorithm.

-> Example:

{
  "alg": "HS256",
  "typ": "JWT"
}

Payload contains the claims. Claims are statements about an entity and additional information.
There are 3 types of claims ->

  • Registered claims -> These are a set of predefined claims: iss (issuer), exp (expiration time), sub (subject)
  • Public claims
  • Private claims

Example ->

{
  "id": 3,
  "iat": 1538339534,
  "exp": 1538425934
}

Signature -> To create the signature part you have to take the encoded header, the encoded payload, a secret, the algorithm specified in the header, and sign that.

Example ->

HMACSHA512(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  your-256-bit-secret
)

Combine all together, we get 3 Base64-URL strings separated by dots,

Example:

– Encoded ->

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MywiaWF0IjoxNTM4MzM5NTM0LCJleHAiOjE1Mzg0MjU5MzR9.wKse6-ERNP4g_sPBdM72GZgpNpHH87UGbzYH3_0mdpo

– Decoded ->

nodejs-jwt-authentication-express-bcryptjs-jsonwebtoken-mongoose-decoded-token

Overview

Project Structure

nodejs-jwt-authentication-express-bcryptjs-jsonwebtoken-mongoose-project-structure

  • config package defines MongoDB Database Configuration, JWT Secret Key & User Roles.
  • model package defines Role & User Mongoose models.
  • router package defines RestAPI URLs, verification functions for signup, & verification JWT token function for signin.
  • controller package defines proccesing functions for each RestAPIs declared in router package.
Workflow

We will define 5 workflows as below ->

  • SignUp Scenarios:
    	-> Verify UserName & Email
    	  -> If NOT Duplicate (UserName & Email), verify Roles are existed.
    		-> If Roles are available, save User Info to database by Sequlieze ORM
    	-> Othercase, Eror code will be returned
    

    – Code in router.js ->

    app.post('/api/auth/signup', [verifySignUp.checkDuplicateUserNameOrEmail, verifySignUp.checkRolesExisted], controller.signup);
    
  • SignIn Scenarios:
    	-> Find User record in database by username
    		-> If User is existed, check password is Valid or NOT
    			-> If password is valid, create JWT then return JWT token back to client
    	-> Othercase, Error code will be returned
    

    – Code in router.js ->

    app.post('/api/auth/signin', controller.signin);
    
  • Access User Content:
    	-> Verify JWT Token
    		-> If token is valid, controller will load & return User Info back to client
    	-> Othercase, Error Code will be returned
    

    – Code in router.js ->

    app.get('/api/test/user', [authJwt.verifyToken], controller.userContent);
    
  • Access PM Content:
    	-> Verify JWT Token
    		-> If token is valid, verify PM role.
    			-> If User has Admin or PM role, controller will load & return Management Content to client.
    	-> Othercase, Error code will be returned
    

    – Code in router.js ->

    app.get('/api/test/pm', [authJwt.verifyToken, authJwt.isPmOrAdmin], controller.managementBoard);
    
  • Access Admin Content
    	-> Verify JWT Token
    		-> If token is valid, verify ADMIN role.
    			-> If User has Admin role, controller will load & return Admin Content to client.
    	-> Othercase, Error code will be returned
    

    – Code in router.js ->

    app.get('/api/test/admin', [authJwt.verifyToken, authJwt.isAdmin], controller.adminBoard);
    
Goal

Sign Up ->

nodejs-jwt-authentication-express-bcryptjs-jsonwebtoken-mongoose-ADAM-sign-up

Sign In ->

nodejs-jwt-authentication-express-bcryptjs-jsonwebtoken-mongoose-ADAM-Sign-In

Access API Successfully ->

nodejs-jwt-authentication-express-bcryptjs-jsonwebtoken-mongoose-ADAM-access-User-API-successfully

Unauthorized Access ->

nodejs-jwt-authentication-express-bcryptjs-jsonwebtoken-mongoose-ADAM-can-NOT-access-PM-apis

Practice

Create Nodejs Project

Following the guide to create a NodeJS/Express project

Install Express, Mongoose, Json Web Token, Bcryptjs:

$npm install express mongoose jsonwebtoken bcryptjs --save

-> package.json file:

{
  "name": "nodejs-jwt-auth",
  "version": "1.0.0",
  "description": "Nodejs-JWT-Authentication-with-MongoDB-Mongoose-ORM",
  "main": "server.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [
    "Nodejs",
    "Express",
    "JWT",
    "Mongoose",
    "MongoDB",
    "Authentication"
  ],
  "author": "ozenero.com",
  "license": "ISC",
  "dependencies": {
    "bcryptjs": "^2.4.3",
    "express": "^4.16.3",
    "jsonwebtoken": "^8.3.0",
    "mongoose": "^5.3.1",
  }
}
Create Mongoose Models

User model ->

const Role = require('./role.model.js');
const mongoose = require('mongoose'), Schema = mongoose.Schema;
 
const UserSchema = mongoose.Schema({
		name: String,
		username: String,
		email: String,
		password: String,
		roles: [{ type: Schema.Types.ObjectId, ref: 'Role' }]
});

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

Role model:

const mongoose = require('mongoose'), Schema = mongoose.Schema;
 
const RoleSchema = mongoose.Schema({
    name: String
});
 
module.exports = mongoose.model('Role', RoleSchema);
Project Configuration

/app/config/config.js file ->

module.exports = {
  'secret': 'ozenero-super-secret-key',
  url: 'mongodb://localhost:27017/gkzdb',
  ROLEs: ['USER', 'ADMIN', 'PM']
};
Define RestAPIs Router

We define 5 RestAPIs in /app/router/router.js

const verifySignUp = require('./verifySignUp');
const authJwt = require('./verifyJwtToken');

module.exports = function(app) {

    const controller = require('../controller/controller.js');
 
	app.post('/api/auth/signup', [verifySignUp.checkDuplicateUserNameOrEmail, verifySignUp.checkRolesExisted], controller.signup);
	
	app.post('/api/auth/signin', controller.signin);
	
	app.get('/api/test/user', [authJwt.verifyToken], controller.userContent);
	
	app.get('/api/test/pm', [authJwt.verifyToken, authJwt.isPmOrAdmin], controller.managementBoard);
	
	app.get('/api/test/admin', [authJwt.verifyToken, authJwt.isAdmin], controller.adminBoard);
}

We need implement middleware functions to do a verification for SignUp & SignIn:

/app/router/verifySignUp.js implements 2 middleware functions:

  • checkDuplicateUserNameOrEmail -> checking the posted username or email is duplicated or NOT
  • checkRolesExisted -> checking the posted User Role is existed or NOT
const config = require('../config/config.js');
const ROLEs = config.ROLEs; 
const User = require('../model/user.model.js');

checkDuplicateUserNameOrEmail = (req, res, next) => {
	// -> Check Username is already in use

	User.findOne({ username: req.body.username })
	.exec((err, user) => {
		if (err && err.kind !== 'ObjectId'){
			res.status(500).send({
				message: "Error retrieving User with Username = " + req.body.username
			});                
			return;
		}

		if(user){
			res.status(400).send("Fail -> Username is already taken!");
			return;
		}

		User.findOne({ email: req.body.email })
		.exec((err, user) => {
			if (err && err.kind !== 'ObjectId'){
				res.status(500).send({
					message: "Error retrieving User with Email = " + req.body.email
				});                
				return;
			}
	
			if(user){
				res.status(400).send("Fail -> Email is already in use!");
				return;
			}

			next();
		});
	});
}

checkRolesExisted = (req, res, next) => {	
	for(let i=0; i Does NOT exist Role = " + req.body.roles[i]);
			return;
		}
	}
	next();
}

const signUpVerify = {};
signUpVerify.checkDuplicateUserNameOrEmail = checkDuplicateUserNameOrEmail;
signUpVerify.checkRolesExisted = checkRolesExisted;

module.exports = signUpVerify;

/app/router/verifyJwtToken.js implements 3 middleware functions:

  • verifyToken -> checking a JWT token is valid or NOT
  • isAdmin -> checking an User has ADMIN role or NOT
  • isPmOrAdmin -> checking an User has PM or ADMIN role or NOT
const jwt = require('jsonwebtoken');
const config = require('../config/config.js');
const User = require('../model/user.model.js');
const Role = require('../model/role.model.js');

verifyToken = (req, res, next) => {
	let token = req.headers['x-access-token'];
  
	if (!token){
		return res.status(403).send({ 
			auth: false, message: 'No token provided.' 
		});
	}

	jwt.verify(token, config.secret, (err, decoded) => {
		if (err){
			return res.status(500).send({ 
					auth: false, 
					message: 'Fail to Authentication. Error -> ' + err 
				});
		}
		req.userId = decoded.id;
		next();
	});
}

isAdmin = (req, res, next) => {
	
	User.findOne({ _id: req.userId })
	.exec((err, user) => {
		if (err){
			if(err.kind === 'ObjectId') {
				return res.status(404).send({
					message: "User not found with Username = " + req.body.username
				});                
			}
			return res.status(500).send({
				message: "Error retrieving User with Username = " + req.body.username
			});
		}

		Role.find({
			'_id': { $in: user.roles }
		}, (err, roles) => {
			if(err) 
				res.status(500).send("Error -> " + err);

			for(let i=0; i {
	User.findOne({ _id: req.userId })
	.exec((err, user) => {
		if (err){
			if(err.kind === 'ObjectId') {
				return res.status(404).send({
					message: "User not found with Username = " + req.body.username
				});                
			}
			return res.status(500).send({
				message: "Error retrieving User with Username = " + req.body.username
			});
		}
		
		Role.find({
			'_id': { $in: user.roles }
		}, (err, roles) => {
			if(err) 
				res.status(500).send("Error -> " + err);

			for(let i=0; i
Implement Controller

- /app/controller/controller.js exports 5 funtions:

  • signup -> be used to register new User
  • signin -> be used to Login
  • userContent -> get User Info
  • managementBoard -> get Management Board Content
  • adminBoard -> get Admin Board Content
const config = require('../config/config.js');

const Role = require('../model/role.model.js');
const User = require('../model/user.model.js');

var jwt = require('jsonwebtoken');
var bcrypt = require('bcryptjs');

exports.signup = (req, res) => {
	// Save User to Database
	console.log("Processing func -> SignUp");

	const user = new User({
		name: req.body.name,
		username: req.body.username,
		email: req.body.email,
		password: bcrypt.hashSync(req.body.password, 8)
	});

    // Save a User to the MongoDB
    user.save().then(savedUser => {
		Role.find({
				'name': { $in: req.body.roles.map(role => role.toUpperCase()) }
		}, (err, roles) => {
			if(err) 
				res.status(500).send("Error -> " + err);

			// update User with Roles
			savedUser.roles = roles.map(role => role._id);
			savedUser.save(function (err) {
				if (err) 
					res.status(500).send("Error -> " + err);

				res.send("User registered successfully!");
			});
		});
    }).catch(err => {
        res.status(500).send("Fail! Error -> " + err);
    });
}

exports.signin = (req, res) => {
	console.log("Sign-In");

	User.findOne({ username: req.body.username })
	.exec((err, user) => {
		if (err){
			if(err.kind === 'ObjectId') {
				return res.status(404).send({
					message: "User not found with Username = " + req.body.username
				});                
			}
			return res.status(500).send({
				message: "Error retrieving User with Username = " + req.body.username
			});
		}
					
		var passwordIsValid = bcrypt.compareSync(req.body.password, user.password);
		if (!passwordIsValid) {
			return res.status(401).send({ auth: false, accessToken: null, reason: "Invalid Password!" });
		}
		
		var token = jwt.sign({ id: user._id }, config.secret, {
		  expiresIn: 86400 // expires in 24 hours
		});
		
		res.status(200).send({ auth: true, accessToken: token });
	});
}

exports.userContent = (req, res) => {
	User.findOne({ _id: req.userId })
	.select('-_id -__v -password')
	.populate('roles', '-_id -__v')
	.exec((err, user) => {
		if (err){
			if(err.kind === 'ObjectId') {
				return res.status(404).send({
					message: "User not found with _id = " + req.userId
				});                
			}
			return res.status(500).send({
				message: "Error retrieving User with _id = " + req.userId	
			});
		}
					
		res.status(200).json({
			"description": "User Content Page",
			"user": user
		});
	});
}

exports.adminBoard = (req, res) => {
	User.findOne({ _id: req.userId })
	.select('-_id -__v -password')
	.populate('roles', '-_id -__v')
	.exec((err, user) => {
		if (err){
			if(err.kind === 'ObjectId') {
				res.status(404).send({
					message: "User not found with _id = " + req.userId
				});                
				return;
			}

			res.status(500).json({
				"description": "Can not access Admin Board",
				"error": err
			});

			return;
		}
					
		res.status(200).json({
			"description": "Admin Board",
			"user": user
		});
	});
}

exports.managementBoard = (req, res) => {
	User.findOne({ _id: req.userId })
	.select('-_id -__v -password')
	.populate('roles', '-_id -__v')
	.exec((err, user) => {
		if (err){
			if(err.kind === 'ObjectId') {
				res.status(404).send({
					message: "User not found with _id = " + req.userId
				});                
				return;
			}

			res.status(500).json({
				"description": "Can not access PM Board",
				"error": err
			});

			return;
		}
					
		res.status(200).json({
			"description": "PM Board",	
			"user": user
		});
	});
}
Server

- /app/server.js file ->

var express = require('express');
var app = express();
var bodyParser = require('body-parser');
app.use(bodyParser.json())
 
require('./app/router/router.js')(app);

const Role = require('./app/model/role.model.js');
  
// Configuring the database
const config = require('./app/config/config.js');
const mongoose = require('mongoose');
 
mongoose.Promise = global.Promise;
 
// Connecting to the database
mongoose.connect(config.url)
.then(() => {
	console.log("Successfully connected to MongoDB.");    
	initial();
}).catch(err => {
    console.log('Could not connect to MongoDB.');
    process.exit();	
});
 
// Create a Server
var server = app.listen(8080, function () {
 
  var host = server.address().address
  var port = server.address().port
 
  console.log("App listening at http://%s:%s", host, port)
})


function initial(){
	Role.count( (err, count) => {
		if(!err && count === 0) {
			// USER Role ->
			new Role({
				name: 'USER'
			}).save( err => {
				if(err) return console.error(err.stack)
				console.log("USER_ROLE is added")
			});

			// ADMIN Role ->
			new Role({
				name: 'ADMIN'
			}).save( err => {
				if(err) return console.error(err.stack)
				console.log("ADMIN_ROLE is added")
			});

			// PM Role ->
			new Role({
				name: 'PM'
			}).save(err => {
				if(err) return console.error(err.stack)
				console.log("PM_ROLE is added")
			});
		}
	});
}
Run & Check Results
Start Nodejs Server

- Run Nodejs server by cmd npm start.

-> Check MongoDB database:

nodejs-jwt-authentication-express-bcryptjs-jsonwebtoken-mongoose-database-after-initial

Sign Up

nodejs-jwt-authentication-express-bcryptjs-jsonwebtoken-mongoose-ADAM-sign-up

-> MongoDB records:

nodejs-jwt-authentication-express-bcryptjs-jsonwebtoken-mongoose-database-after-sign-up-all-user-records

SignIn and Access Protected Resources

- Adam can access api/test/user url, can NOT access others.

-> Sign In:

nodejs-jwt-authentication-express-bcryptjs-jsonwebtoken-mongoose-ADAM-Sign-In

-> Access Protected Resources:

nodejs-jwt-authentication-express-bcryptjs-jsonwebtoken-mongoose-ADAM-access-User-API-successfully

nodejs-jwt-authentication-express-bcryptjs-jsonwebtoken-mongoose-ADAM-can-NOT-access-PM-apis

- Jack can access api/test/user & api/test/pm url.
Can NOT access /api/test/admin url.

-> Sign In:

nodejs-jwt-authentication-express-bcryptjs-jsonwebtoken-mongoose-JACK-sign-in

-> Access Protected Resources:

nodejs-jwt-authentication-express-bcryptjs-jsonwebtoken-mongoose-JACK-access-User-API-successfully

nodejs-jwt-authentication-express-bcryptjs-jsonwebtoken-mongoose-JACK-access-PM-API-successfully

nodejs-jwt-authentication-express-bcryptjs-jsonwebtoken-mongoose-JACK-can-NOT-access-ADMIN-API

Thomas can access all URLs.

nodejs-jwt-authentication-express-bcryptjs-jsonwebtoken-mongoose-THOMAS-sign-in

-> Access Protected Resource:

nodejs-jwt-authentication-express-bcryptjs-jsonwebtoken-mongoose-THOMAS-access-ADMIN-API-successfully

SourceCode

Node.js-JWT-Auth-Mongoose

0 0 votes
Article Rating
Subscribe
Notify of
guest
987 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments