Angular & Nodejs JWT Authentication fullstack | Nodejs/Express RestAPIs + JWT + BCryptjs + Sequelize + MySQL – Part 2: Build Backend

angular-nodejs-jwt-authentication-tutorial-feature-image

The tutorial is Part 2 of the series: Angular & Nodejs JWT Authentication fullstack | Nodejs/Express RestAPIs + JWT + BCryptjs + Sequelize + MySQL. Today we’re gonna build a Nodejs Authentication & Authorization RestAPIs that can interact with MySQL database.

Part 1: Overview and Architecture.
Part 3: Build Frontend

JWT Authentication with Nodejs/Express RestAPIs

Demo

Overview

angular-nodejs-jwt-authentication-architecture-diagram-back-end-server

HTTP request that matches route will be accepted by CORS Middleware before coming to Security layer.

Security layer includes:
JWT Authentication Middleware: verify SignUp, verify token
Authorization Middleware: check User’s roles

Main Business Logic Processing interacts with database via Sequelize and send HTTP response (token, user information, data based on roles…) to client.

Config Middleware & RestAPIs
module.exports = function (app) {

	const controller = require('../controller/controller.js');

	app.use(function (req, res, next) {
		res.header("Access-Control-Allow-Origin", "*");
		res.header("Access-Control-Allow-Headers", "x-access-token, Origin, Content-Type, Accept");
		next();
	});

	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);
}

– For HTTP Header, we allow x-access-token for JWT.
– When a HTTP request call /signup api, it will also be passed to checkDuplicateUserNameOrEmail() and checkRolesExisted() funtions before going to controller’s signup() funtion.
– JWT Authentication middleware with verifyToken() and role checking funtions (isPmOrAdmin, isAdmin) will be called before controller returns authorized data (based on roles).

Generate Token

Inside controller’s signin() funtion, we use sign() funtion from jsonwebtoken:

var jwt = require('jsonwebtoken');

exports.signin = (req, res) => {
	User.findOne({
		where: { username: req.body.username }
	}).then(user => {
		// check user & password...

		var token = jwt.sign({ id: user.id }, config.secret, {
			expiresIn: 86400 // expires in 24 hours
		});

		// get other user information
		res.status(200).send({
			auth: true,
			accessToken: token,
			username: user.username,
			authorities: authorities
		});
	});
}
Verify Token

We get token from x-access-token of HTTP headers, then use verify() function of jsonwebtoken:

const jwt = require('jsonwebtoken');

verifyToken = (req, res, next) => {
	let token = req.headers['x-access-token'];
  
	if (!token){ // notice that no token was provided...}

	jwt.verify(token, 'SECRET KEY', (err, decoded) => {
		if (err){
			return res.status(500).send({ 
					auth: false, 
					message: 'Fail to Authentication. Error -> ' + err 
				});
		}
		req.userId = decoded.id;
		next();
	});
}
User & Roles Relationship model

We define Role & User Sequelize models as below:

angular-nodejs-jwt-authentication-tutorial-back-end-sequelize-many-to-many-user-role

Implementation of the Many-to-Many relationship:

// user.model.js
module.exports = (sequelize, Sequelize) => {
	const User = sequelize.define('users', {
	  name: {
		  type: Sequelize.STRING
	  },
	  username: {
		  type: Sequelize.STRING
	  },
	  email: {
		  type: Sequelize.STRING
	  },
	  password: {
		  type: Sequelize.STRING
	  }
	});
	
	return User;
}

// role.model.js
module.exports = (sequelize, Sequelize) => {
	const Role = sequelize.define('roles', {
	  id: {
        type: Sequelize.INTEGER,
        primaryKey: true
	  },
	  name: {
		  type: Sequelize.STRING
	  }
	});
	
	return Role;
}

// db.config.js
const Sequelize = require('sequelize');
const sequelize = new Sequelize(...);
 
const db = {};
 
db.Sequelize = Sequelize;
db.sequelize = sequelize;
 
db.user = require('../model/user.model.js')(sequelize, Sequelize);
db.role = require('../model/role.model.js')(sequelize, Sequelize);
 
db.role.belongsToMany(db.user, { through: 'user_roles', foreignKey: 'roleId', otherKey: 'userId'});
db.user.belongsToMany(db.role, { through: 'user_roles', foreignKey: 'userId', otherKey: 'roleId'});

module.exports = db;

Nodejs server for JWT Authentication example Overview

Goal

The diagram below show how our system handles User Registration and User Login processes:

angular-nodejs-jwt-authentication-example-work-process-diagram

/api/auth/signup:

angular-nodejs-jwt-authentication-tutorial-back-end-signup-result

/api/auth/signin:

angular-nodejs-jwt-authentication-tutorial-back-end-signin-result

/api/test/user:

angular-nodejs-jwt-authentication-tutorial-back-end-get-user-content-result

/api/test/pm:

angular-nodejs-jwt-authentication-tutorial-back-end-get-pm-content-result

/api/test/admin:

angular-nodejs-jwt-authentication-tutorial-back-end-get-admin-content-result

Technologies

– Nodejs/Express
– Json Web Token
– BCryptjs
– Sequelize
– MySQL

Project Structure

angular-nodejs-jwt-authentication-tutorial-back-end-project-structure

config package defines MySQL Database Configuration, JWT Secret Key & User Roles.
model package defines Role & User Sequelize models.
router package defines RestAPI URLs, verification functions for signup api, JWT verification for signin api, and authorization functions for content requested by user roles.
controller package defines process functions for each RestAPIs declared in router package.

Practice

Create Nodejs Project

Following the guide to creating a NodeJS/Express project.

Install Express, Sequelize, MySQL, Json Web Token, Bcryptjs:
$npm install express sequelize mysql2 jsonwebtoken bcryptjs --save

package.json

{
  "name": "nodejs-jwt-auth",
  "version": "1.0.0",
  "description": "Nodejs-JWT-Authentication-with-MySQL-Sequelize-ORM",
  "main": "server.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [
    "Nodejs",
    "Express",
    "JWT",
    "Sequelize",
    "MySQL",
    "Authentication"
  ],
  "author": "grokonez.com",
  "license": "ISC",
  "dependencies": {
    "bcryptjs": "^2.4.3",
    "express": "^4.16.3",
    "jsonwebtoken": "^8.3.0",
    "mysql2": "^1.6.1",
    "sequelize": "^4.39.0"
  }
}
Create Sequelize Models
User

model/user.model.js

module.exports = (sequelize, Sequelize) => {
	const User = sequelize.define('users', {
	  name: {
		  type: Sequelize.STRING
	  },
	  username: {
		  type: Sequelize.STRING
	  },
	  email: {
		  type: Sequelize.STRING
	  },
	  password: {
		  type: Sequelize.STRING
	  }
	});
	
	return User;
}
Role

model/role.model.js

module.exports = (sequelize, Sequelize) => {
	const Role = sequelize.define('roles', {
	  id: {
        type: Sequelize.INTEGER,
        primaryKey: true
	  },
	  name: {
		  type: Sequelize.STRING
	  }
	});
	
	return Role;
}
Sequelize Database Configuration

config/env.js

const env = {
  database: 'testdb',
  username: 'root',
  password: '123456',
  host: 'localhost',
  dialect: 'mysql',
  pool: {
	  max: 5,
	  min: 0,
	  acquire: 30000,
	  idle: 10000
  }
};
 
module.exports = env;

config/db.config.js

const env = require('./env.js');
 
const Sequelize = require('sequelize');
const sequelize = new Sequelize(env.database, env.username, env.password, {
  host: env.host,
  dialect: env.dialect,
  operatorsAliases: false,
 
  pool: {
    max: env.max,
    min: env.pool.min,
    acquire: env.pool.acquire,
    idle: env.pool.idle
  }
});
 
const db = {};
 
db.Sequelize = Sequelize;
db.sequelize = sequelize;
 
db.user = require('../model/user.model.js')(sequelize, Sequelize);
db.role = require('../model/role.model.js')(sequelize, Sequelize);
 
db.role.belongsToMany(db.user, { through: 'user_roles', foreignKey: 'roleId', otherKey: 'userId'});
db.user.belongsToMany(db.role, { through: 'user_roles', foreignKey: 'userId', otherKey: 'roleId'});

module.exports = db;

More details at: Sequelize Many-to-Many association – NodeJS/Express, MySQL

Define RestAPIs Router with Middleware
RestAPIs Router

router/router.js

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

module.exports = function (app) {

	const controller = require('../controller/controller.js');

	app.use(function (req, res, next) {
		res.header("Access-Control-Allow-Origin", "*");
		res.header("Access-Control-Allow-Headers", "x-access-token, Origin, Content-Type, Accept");
		next();
	});

	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);
}
Middleware functions

router/verifySignUp.js

const db = require('../config/db.config.js');
const config = require('../config/config.js');
const ROLEs = config.ROLEs;
const User = db.user;

checkDuplicateUserNameOrEmail = (req, res, next) => {
	// -> Check Username is already in use
	User.findOne({
		where: {
			username: req.body.username
		}
	}).then(user => {
		if (user) {
			res.status(400).send("Fail -> Username is already taken!");
			return;
		}

		// -> Check Email is already in use
		User.findOne({
			where: {
				email: req.body.email
			}
		}).then(user => {
			if (user) {
				res.status(400).send("Fail -> Email is already in use!");
				return;
			}

			next();
		});
	});
}

checkRolesExisted = (req, res, next) => {
	for (let i = 0; i < req.body.roles.length; i++) {
		if (!ROLEs.includes(req.body.roles[i].toUpperCase())) {
			res.status(400).send("Fail -> Does NOT exist Role = " + req.body.roles[i]);
			return;
		}
	}
	next();
}

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

module.exports = signUpVerify;

router/verifyJwtToken.js

const jwt = require('jsonwebtoken');
const config = require('../config/config.js');
const db = require('../config/db.config.js');
const User = db.user;

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.findById(req.userId)
		.then(user => {
			user.getRoles().then(roles => {
				for(let i=0; i {
	User.findById(req.userId)
		.then(user => {
			user.getRoles().then(roles => {
				for(let i=0; i
Implement RestApis Controller

controller/controller.js

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

const Op = db.Sequelize.Op;

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

exports.signup = (req, res) => {
	// Save User to Database
	User.create({
		name: req.body.name,
		username: req.body.username,
		email: req.body.email,
		password: bcrypt.hashSync(req.body.password, 8)
	}).then(user => {
		Role.findAll({
			where: {
				name: {
					[Op.or]: req.body.roles
				}
			}
		}).then(roles => {
			user.setRoles(roles).then(() => {
				res.send({ message: 'Registered successfully!' });
			});
		}).catch(err => {
			res.status(500).send({ reason: err.message });
		});
	}).catch(err => {
		res.status(500).send({ reason: err.message });
	})
}

exports.signin = (req, res) => {
	User.findOne({
		where: {
			username: req.body.username
		}
	}).then(user => {
		if (!user) {
			return res.status(404).send({ reason: 'User Not Found.' });
		}

		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
		});

		var authorities = [];
		user.getRoles().then(roles => {
			for (let i = 0; i < roles.length; i++) {
				authorities.push('ROLE_' + roles[i].name.toUpperCase());
			}
			res.status(200).send({
				auth: true,
				accessToken: token,
				username: user.username,
				authorities: authorities
			});
		})
	}).catch(err => {
		res.status(500).send({ reason: err.message });
	});
}

exports.userContent = (req, res) => {
	User.findOne({
		where: { id: req.userId },
		attributes: ['name', 'username', 'email'],
		include: [{
			model: Role,
			attributes: ['id', 'name'],
			through: {
				attributes: ['userId', 'roleId'],
			}
		}]
	}).then(user => {
		res.status(200).send({
			'description': '>>> User Contents!',
			'user': user
		});
	}).catch(err => {
		res.status(500).send({
			'description': 'Can not access User Page',
			'error': err
		});
	})
}

exports.adminBoard = (req, res) => {
	User.findOne({
		where: { id: req.userId },
		attributes: ['name', 'username', 'email'],
		include: [{
			model: Role,
			attributes: ['id', 'name'],
			through: {
				attributes: ['userId', 'roleId'],
			}
		}]
	}).then(user => {
		res.status(200).send({
			'description': '>>> Admin Contents',
			'user': user
		});
	}).catch(err => {
		res.status(500).send({
			'description': 'Can not access Admin Board',
			'error': err
		});
	})
}

exports.managementBoard = (req, res) => {
	User.findOne({
		where: { id: req.userId },
		attributes: ['name', 'username', 'email'],
		include: [{
			model: Role,
			attributes: ['id', 'name'],
			through: {
				attributes: ['userId', 'roleId'],
			}
		}]
	}).then(user => {
		res.status(200).send({
			'description': '>>> Project Management Board',
			'user': user
		});
	}).catch(err => {
		res.status(500).send({
			'description': 'Can not access Management Board',
			'error': err
		});
	})
}

We define jwt-secret-key & User Roles in config/config.js:

module.exports = {
  'secret': 'grokonez-super-secret-key',
  ROLEs: ['USER', 'ADMIN', 'PM']
};
Server

server.js

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

require('./app/router/router.js')(app);

const db = require('./app/config/db.config.js');

const Role = db.role;

// force: true will drop the table if it already exists
db.sequelize.sync({force: true}).then(() => {
  console.log('Drop and Resync with { force: true }');
  initial();
});

// 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.create({
		id: 1,
		name: "USER"
	});

	Role.create({
		id: 2,
		name: "PM"
	});

	Role.create({
		id: 3,
		name: "ADMIN"
	});
}

SourceCode

Nodejs-JWT-Authentication



By grokonez | December 10, 2018.


Related Posts


Got Something To Say:

Your email address will not be published. Required fields are marked *

*