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
Contents
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": "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 ->
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{ User.findById(req.userId) .then(user => { user.getRoles().then(roles => { for(let i=0; i 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 Contentconst 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 definesjwt-secret-key
& User Roles.module.exports = { 'secret': 'grokonez-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@grokonez.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@grokonez.com' LIMIT 1; Processing func -> SignUp Executing (default): INSERT INTO `users` (`id`,`name`,`username`,`email`,`password`,`createdAt`,`updatedAt`) VALUES (DEFAULT,'Jack','jackgkz','jack@grokonez.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@grokonez.com' LIMIT 1; Processing func -> SignUp Executing (default): INSERT INTO `users` (`id`,`name`,`username`,`email`,`password`,`createdAt`,`updatedAt`) VALUES (DEFAULT,'Thomas','thomasgkz','thomas@grokonez.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:
SourceCode
Last updated on February 6, 2020.
Hello, i am getting an error on getRoles(), Unhandled rejection TypeError: Cannot read property ‘getRoles’ of null, if you can help me it will be great ! Really great tutorial on authentication.
Hi friend, I has the same problem, I tried to fix It and I got It, the problem is because in the relationship (User Role) you should call with include Rol model in FindByPK (FindByID) with this your problem is fixed. this is the complete function:
I hope that is not late to fix .
King regards.
Judlup
hi, nice try. but your code still give me error like admin unable to request all url. is yours could?
Great Tutorial!
Does this support database migration?
If so, how can I add/edit/remove a field in a table without losing records?
Thanks
Hi,
Many thanks for this tutorial, it helps me a lot for my project.
It’s really well explained and illustrated !
Thanks
Hi the config.js file is missing?
where is the code for that file
Thanks
How about logout
How about logout and update of password/change of password by user?
You have done a good job, kindly finish the help for us.
This is an amazing tutorial. Thank you so much.
thank u so much
please add functional for logout
Hello, is there anyone can modify the same code which will be having only two roles say Admin and Volunteer and can access these as same as in the above code.
Copied this exactly however when testing sign up I recieve this error and the App crashes:
Unhandled rejection Error: WHERE parameter “username” has invalid “undefined” value
I got the same issue ..
when you’re sending data on postman select x-www-form-urlencoded instead of raw then add
app.use(bodyParser.urlencoded({extended: true})); after app.use(bodyParser.json()); in server.js
I found this problem :
SequelizeConnectionRefusedError: connect ECONNREFUSED 127.0.0.1:3306
start your mysql server .cmd :service mysql start
I have a problem Fail -> Does NOT exist Role
I am getting Error -> TypeError: user.setRoles is not a function in the controller.js signup function
rhpsngrgcxhvndpxydpqaswpuxdmkq
Where is the github code for this
I want to hit a custom query then how can I do?
Great tutorial, Please upgrade the password encryption and decryption with the async methods of bcryptjs package.