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).
Related posts:
– Sequelize Many-to-Many association – NodeJS/Express, MySQL
– Sequelize ORM – Build CRUD RestAPIs with NodeJs/Express, Sequelize, MySQL
– Fullstack with Angular: Angular & Nodejs JWT Authentication fullstack
Technologies
– Nodejs/Express
– Json Web Token
– BCryptjs
– Sequelize
– MySQL
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 ->
Overview
Demo
Project Structure
config
package defines MySQL Database Configuration, JWT Secret Key & User Roles.model
package definesRole
&User
Sequelize 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 inrouter
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, checkpassword
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 ->
Sign In ->
Access API Successfully ->
Unauthorized Access ->
Practice
Create Nodejs Project
Following the guide to create a NodeJS/Express 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": "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": "ozenero.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 ->
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 ->
const env = { database: 'testdb', username: 'root', password: '12345', host: 'localhost', dialect: 'mysql', 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.
-> See more at: Sequelize Many-to-Many association – NodeJS/Express, MySQL
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 postedusername
oremail
is duplicated or NOTcheckRolesExisted
-> checking the posted UserRole
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; iDoes 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 NOTisAdmin
-> checking an User hasADMIN
role or NOTisPmOrAdmin
-> checking an User hasPM
orADMIN
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) => { User.findById(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) => { User.findById(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 Controller
– /app/controller/controller.js
exports 5 funtions:
signup
-> be used to register new Usersignin
-> be used to LoginuserContent
-> get User InfomanagementBoard
-> get Management Board ContentadminBoard
-> 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': 'ozenero-super-secret-key', ROLEs: ['USER', 'ADMIN', 'PM'] };
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:
npm start > nodejs-jwt-auth@1.0.0 start D:\gkz\article\Nodejs-JWT-Authentication\nodejs-jwt-auth > node server.js 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), `username` 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 NULL, `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 NULL, `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 CASCADE) 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 (1,'USER','2018-09-30 20:11:40','2018-09-30 20:11:40'); Executing (default): INSERT INTO `roles` (`id`,`name`,`createdAt`,`updatedAt`) VALUES (2,'ADMIN','2018-09-30 20:11:40','2018-09-30 20:11:40'); Executing (default): INSERT INTO `roles` (`id`,`name`,`createdAt`,`updatedAt`) VALUES (3,'PM','2018-09-30 20:11:40','2018-09-30 20:11:40');
-> Check MySQL database:
Sign Up
-> All Logs of Sign Up:
Processing func -> SignUp Executing (default): INSERT INTO `users` (`id`,`name`,`username`,`email`,`password`,`createdAt`,`updatedAt`) VALUES (DEFAULT,'Adam','adamgkz','adam@ozenero.com','$2a$08$qJts8G2RD7/J6RJGIPKxRuAKJTI1.C0WK93cvPQY0xutx6DWXv.PW','2018-09-30 20:14:08','2018-09-30 20:14:08'); 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_roles`.`userId` = 1; Executing (default): INSERT INTO `user_roles` (`createdAt`,`updatedAt`,`roleId`,`userId`) VALUES ('2018-09-30 20:14:08','2018-09-30 20:14:08',1,1); Executing (default): SELECT `id`, `name`, `username`, `email`, `password`, `createdAt`, `updatedAt` FROM `users` AS `users` WHERE `users`.`username` = 'jackgkz' LIMIT 1; Executing (default): SELECT `id`, `name`, `username`, `email`, `password`, `createdAt`, `updatedAt` FROM `users` AS `users` WHERE `users`.`email` = 'jack@ozenero.com' LIMIT 1; Processing func -> SignUp Executing (default): INSERT INTO `users` (`id`,`name`,`username`,`email`,`password`,`createdAt`,`updatedAt`) VALUES (DEFAULT,'Jack','jackgkz','jack@ozenero.com','$2a$08$vr8m87P4Lhz4AmewyZEo4uq7zFQWAfg5qPZZq9itzdPPcNjwIy7Gu','2018-09-30 20:15:41','2018-09-30 20:15:41'); Executing (default): SELECT `id`, `name`, `createdAt`, `updatedAt` FROM `roles` AS `roles` WHERE (`roles`.`name` = 'pm'); Executing (default): SELECT `createdAt`, `updatedAt`, `roleId`, `userId` FROM `user_roles` AS `user_roles` WHERE `user_roles`.`userId` = 2; Executing (default): INSERT INTO `user_roles` (`createdAt`,`updatedAt`,`roleId`,`userId`) VALUES ('2018-09-30 20:15:41','2018-09-30 20:15:41',3,2); Executing (default): SELECT `id`, `name`, `username`, `email`, `password`, `createdAt`, `updatedAt` FROM `users` AS `users` WHERE `users`.`username` = 'thomasgkz' LIMIT 1; Executing (default): SELECT `id`, `name`, `username`, `email`, `password`, `createdAt`, `updatedAt` FROM `users` AS `users` WHERE `users`.`email` = 'thomas@ozenero.com' LIMIT 1; Processing func -> SignUp Executing (default): INSERT INTO `users` (`id`,`name`,`username`,`email`,`password`,`createdAt`,`updatedAt`) VALUES (DEFAULT,'Thomas','thomasgkz','thomas@ozenero.com','$2a$08$hMKkxpOfvSIrFlNtPZ4JkuBIlp27CCZyH/6qo7kRhoBetP113b29C','2018-09-30 20:16:11','2018-09-30 20:16:11'); Executing (default): SELECT `id`, `name`, `createdAt`, `updatedAt` FROM `roles` AS `roles` WHERE (`roles`.`name` = 'admin'); Executing (default): SELECT `createdAt`, `updatedAt`, `roleId`, `userId` FROM `user_roles` AS `user_roles` WHERE `user_roles`.`userId` = 3; Executing (default): INSERT INTO `user_roles` (`createdAt`,`updatedAt`,`roleId`,`userId`) VALUES ('2018-09-30 20:16:11','2018-09-30 20:16:11',2,3);
-> MySQL records:
SignIn and Access Protected Resources
– Adam can access api/test/user
url, can NOT access others.
-> Sign In:
-> Access Protected Resources:
– Jack can access api/test/user
& api/test/pm
url.
Can NOT access /api/test/admin
url.
-> Sign In:
-> Access Protected Resources:
– Thomas can access all URLs.
-> Sign In:
-> Access Protected Resource: