Token Based Authentication in Node.js using JWT (JSON Web Tokens) + MySQL Example

Node.js Token Based JWT Authentication using Express with MySQL Example

“How to implement Token Based Authentication in Node.js using JWT (JSON Web Tokens)?” is one of the most common question in the development world? So in the tutorial, I will introduce details step by step how to do it with 100% running coding example by Express RestAPIs, JWT(JSON Web Token), BCryptjs, Sequelize and MySQL/PostgreSQL database.

What will we do?


Overview – Nodejs JWT Express Security Authentication

Technologies

To implement tutorial: ‘Token Based Authentication in Node.js using JWT (JSON Web Tokens)’, we need a set of below technologies:

  • Nodejs/Express
  • Json Web Token
  • BCryptjs
  • Sequelize
  • MySQL/PostgreSQL

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 Sequelize Decoded Token
Nodejs Jwt Authentication Express Bcryptjs Jsonwebtoken Sequelize Decoded Token

Project Structure

Nodejs JWT Authentication Example Project Structure
Nodejs JWT Authentication Example Project Structure

config package defines MySQL/PostgreSQL Database Configuration, JWT Secret Key & User Roles.
model package defines Role & User Sequelize models.

Nodejs Jwt Authentication Many To Many User Role Example
Nodejs Jwt Authentication Many To Many User Role Example

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 – Token Based Authentication in Node.js using JWT (JSON Web Tokens)

We define 5 workflows as below:

  • SignUp Scenarios
  • SignIn Scenarios
  • Access User Content
  • Access PM Content
  • Access Admin Content

– 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

Corresponding 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

Corresponding 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

Corressponding 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

Corresponding 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

Corresponding code in router.js:

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

Goal

– Sign Up:

Register Adam User
Register Adam User

– Sign In

Adam Sign In Successfully
Adam Sign In Successfully

– Access API Successfully

Adam Access User Content Successfully
Adam Access User Content Successfully

– Unauthorized Access

Adam can NOT access PM content
Adam can NOT access PM content

Video Guide – Nodejs JWT Express Security Authentication

Practice – How to do it by Coding?

Create Nodejs Project

– Install Express, Sequelize, MySQL, Json Web Token, Bcryptjs:

$npm install express sequelize mysql2 jsonwebtoken bcryptjs --save

package.json file:

{
  "name": "nodejs-jwt-auth",
  "version": "1.0.0",
  "description": "Build Nodejs JWT Authentication RestAPIs ",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [
    "nodejs",
    "jwt",
    "authentication",
    "restapis"
  ],
  "author": "https://loizenai.com",
  "license": "ISC",
  "dependencies": {
    "bcryptjs": "^2.4.3",
    "express": "^4.17.1",
    "jsonwebtoken": "^8.5.1",
    "mysql2": "^2.1.0",
    "sequelize": "^5.21.12"
  }
}

Create Sequelize Models

User model:

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:

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

Sequelize Database Configuration

/app/config/env.js file :

For MySQL database

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

For PostgreSQL database

const env = {
  database: 'loizenai',
  username: 'postgres',
  password: '123',
  host: 'localhost',
  dialect: 'postgres',
  pool: {
    max: 5,
    min: 0,
    acquire: 30000,
    idle: 10000
  }
};
 
module.exports = env;

/app/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;

Because Role & User has many-to-many association, so we use belongsToMany to configure them.

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 db = require('../config/db.config.js');
const config = require('../config/config.js');
const ROLEs = config.ROLEs; 
const User = db.user;
const Role = db.role;
 
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;

/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 db = require('../config/db.config.js');
const Role = db.role;
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) => {
	let token = req.headers['x-access-token'];
	
	User.findByPk(req.userId)
		.then(user => {
			user.getRoles().then(roles => {
				for(let i=0; i<roles.length; i++){
					console.log(roles[i].name);
					if(roles[i].name.toUpperCase() === "ADMIN"){
						next();
						return;
					}
				}
				
				res.status(403).send("Require Admin Role!");
				return;
			})
		})
}

isPmOrAdmin = (req, res, next) => {
	let token = req.headers['x-access-token'];
	
	User.findByPk(req.userId)
		.then(user => {
			user.getRoles().then(roles => {
				for(let i=0; i<roles.length; i++){					
					if(roles[i].name.toUpperCase() === "PM"){
						next();
						return;
					}
					
					if(roles[i].name.toUpperCase() === "ADMIN"){
						next();
						return;
					}
				}
				
				res.status(403).send("Require PM or Admin Roles!");
			})
		})
}

const authJwt = {};
authJwt.verifyToken = verifyToken;
authJwt.isAdmin = isAdmin;
authJwt.isPmOrAdmin = isPmOrAdmin;

module.exports = authJwt;

Implement Express 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 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
  console.log("Processing func -> SignUp");
  
  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("User registered successfully!");
            });
    }).catch(err => {
      res.status(500).send("Error -> " + err);
    });
  }).catch(err => {
    res.status(500).send("Fail! Error -> " + err);
  })
}
 
exports.signin = (req, res) => {
  console.log("Sign-In");
  
  User.findOne({
    where: {
      username: req.body.username
    }
  }).then(user => {
    if (!user) {
      return res.status(404).send('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
    });
    
    res.status(200).send({ auth: true, accessToken: token });
    
  }).catch(err => {
    res.status(500).send('Error -> ' + err);
  });
}
 
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).json({
      "description": "User Content Page",
      "user": user
    });
  }).catch(err => {
    res.status(500).json({
      "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).json({
      "description": "Admin Board",
      "user": user
    });
  }).catch(err => {
    res.status(500).json({
      "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).json({
      "description": "Management Board",
      "user": user
    });
  }).catch(err => {
    res.status(500).json({
      "description": "Can not access Management Board",
      "error": err
    });
  })
}

– Create /app/config/config.js file that defines jwt-secret-key & User Roles.

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

Nodejs 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 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();
});
 
//require('./app/route/project.route.js')(app);
 
// 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: "ADMIN"
  });
  
  Role.create({
    id: 3,
    name: "PM"
  });
}

Run & Check Results

Start Nodejs Server

– Run Nodejs server by cmd npm start -> Logs:

D:\loizenai.com\nodejs-jwt-auth>npm start

> nodejs-jwt-auth@1.0.0 start D:\loizenai.com\nodejs-jwt-auth
> node server.js

(node:6948) [SEQUELIZE0004] DeprecationWarning: A boolean value was passed to options.operatorsAliases. This is a no-op
with v5 and should be removed.
App listening at http://:::8080
Executing (default): DROP TABLE IF EXISTS `user_roles`;
Executing (default): DROP TABLE IF EXISTS `roles`;
Executing (default): DROP TABLE IF EXISTS `users`;
Executing (default): DROP TABLE IF EXISTS `users`;
Executing (default): CREATE TABLE IF NOT EXISTS `users` (`id` INTEGER NOT NULL auto_increment , `name` VARCHAR(255), `us
ername` VARCHAR(255), `email` VARCHAR(255), `password` VARCHAR(255), `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME
 NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB;
Executing (default): SHOW INDEX FROM `users`
Executing (default): DROP TABLE IF EXISTS `roles`;
Executing (default): CREATE TABLE IF NOT EXISTS `roles` (`id` INTEGER , `name` VARCHAR(255), `createdAt` DATETIME NOT NU
LL, `updatedAt` DATETIME NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB;
Executing (default): SHOW INDEX FROM `roles`
Executing (default): DROP TABLE IF EXISTS `user_roles`;
Executing (default): CREATE TABLE IF NOT EXISTS `user_roles` (`createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NU
LL, `roleId` INTEGER , `userId` INTEGER , PRIMARY KEY (`roleId`, `userId`), FOREIGN KEY (`roleId`) REFERENCES `roles` (`
id`) ON DELETE CASCADE ON UPDATE CASCADE, FOREIGN KEY (`userId`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE C
ASCADE) ENGINE=InnoDB;
Executing (default): SHOW INDEX FROM `user_roles`
Drop and Resync with { force: true }
Executing (default): INSERT INTO `roles` (`id`,`name`,`createdAt`,`updatedAt`) VALUES (?,?,?,?);
Executing (default): INSERT INTO `roles` (`id`,`name`,`createdAt`,`updatedAt`) VALUES (?,?,?,?);
Executing (default): INSERT INTO `roles` (`id`,`name`,`createdAt`,`updatedAt`) VALUES (?,?,?,?);

– Check MySQL database:

Nodejs Jwt Authentication Example - Sequelize Table Schema
Nodejs Jwt Authentication Example – Sequelize Table Schema
Nodejs JWT authentication - All Roles
Nodejs JWT authentication – All Roles

Sign Up

Register Adam User
Register Adam User

All Logs of Sign Up:

Processing func -> SignUp
Executing (default): INSERT INTO `users` (`id`,`name`,`username`,`email`,`password`,`createdAt`,`updatedAt`) VALUES (DEF
AULT,?,?,?,?,?,?);
Executing (default): SELECT `id`, `name`, `createdAt`, `updatedAt` FROM `roles` AS `roles` WHERE (`roles`.`name` = 'user
');
Executing (default): SELECT `createdAt`, `updatedAt`, `roleId`, `userId` FROM `user_roles` AS `user_roles` WHERE `user_r
oles`.`userId` = 1;
Executing (default): INSERT INTO `user_roles` (`createdAt`,`updatedAt`,`roleId`,`userId`) VALUES ('2020-06-08 08:07:03',
'2020-06-08 08:07:03',1,1);

– MySQL records:

Check Mysql tables
Check Mysql tables

SignIn and Access Protected Resources

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

Sign In:

Adam Sign In Successfully
Adam Sign In Successfully

Access Protected Resources:

Adam Access User Content Successfully
Adam Access User Content Successfully
Adam can NOT access PM content
Adam can NOT access PM content

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

Sign In:

Jack Sign In successfully
Jack Sign In successfully

Access Protected Resources:

Jack can access PM page
Jack can access PM page
Jack can NOT access Admin Page
Jack can NOT access Admin Page

Thomas can access all URLs.

Sign In

Thomas sign in successfully
Thomas sign in successfully

Access Protected Resource:

Thomas can access Admin Page
Thomas can access Admin Page

Sourcecode

I includes sourcecode for the ‘Token Based Authentication in Node.js using JWT (JSON Web Tokens) + MySQL Example’ tutorial with below implemented features:

  • Create Sequelize Models
  • Sequelize Database Configuration
  • Define RestAPIs Router
  • Implement Express Controller
  • Node.js Server

Nodejs JWT Authentication Example

Github sourcecode:

Github Sourcecode – Nodejs token based authentication

What will we do?


2 thoughts on “Token Based Authentication in Node.js using JWT (JSON Web Tokens) + MySQL Example”

Leave a Reply

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