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 PostgreSQL.
Related posts:
– Sequelize Many-to-Many association – NodeJS/Express, MySQL
– Node.js/Express RestAPIs CRUD – Sequelize ORM – PostgreSQL
Contents
Technologies
– Nodejs/Express
– Json Web Token
– BCryptjs
– Sequelize
– PosgreSQL
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:
1 2 3 4 |
{ "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 ->
1 2 3 4 5 |
{ "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 ->
1 2 3 4 5 |
HMACSHA512( base64UrlEncode(header) + "." + base64UrlEncode(payload), your-256-bit-secret ) |
Combine all together, we get 3 Base64-URL strings separated by dots,
Example:
– Encoded ->
1 |
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MywiaWF0IjoxNTM4MzM5NTM0LCJleHAiOjE1Mzg0MjU5MzR9.wKse6-ERNP4g_sPBdM72GZgpNpHH87UGbzYH3_0mdpo |
– Decoded ->
See more at: Introduction to JSON Web Tokens
Overview
Project Structure
config
package defines PostgreSQL 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:
1234-> 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
->1app.post('/api/auth/signup', [verifySignUp.checkDuplicateUserNameOrEmail, verifySignUp.checkRolesExisted], controller.signup); - SignIn Scenarios:
1234-> Find User record in database by <code>username-> If User is existed, check <code>password</code> 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
->1app.post('/api/auth/signin', controller.signin); - Access User Content:
123-> Verify JWT Token-> If token is valid, <code>controller</code> will load & return User Info back to client-> Othercase, Error Code will be returned
– Code in
router.js
->1app.get('/api/test/user', [authJwt.verifyToken], controller.userContent); - Access PM Content:
1234-> Verify JWT Token-> If token is valid, verify <code>PM</code> role.-> If User has Admin or PM role, <code>controller</code> will load & return Management Content to client.-> Othercase, Error code will be returned
– Code in
router.js
->1app.get('/api/test/pm', [authJwt.verifyToken, authJwt.isPmOrAdmin], controller.managementBoard); - Access Admin Content
1234-> Verify JWT Token-> If token is valid, verify <code>ADMIN</code> role.-> If User has Admin role, <code>controller</code> will load & return Admin Content to client.-> Othercase, Error code will be returned
– Code in
router.js
->1app.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, PostgreSQL, Json Web Token, Bcryptjs:
1 |
$npm install express sequelize pg pg-hstore jsonwebtoken bcryptjs --save |
-> package.json
file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
{ "name": "nodejs-jwt-auth", "version": "1.0.0", "description": "Nodejs-JWT-Authentication-with-PostgreSQL-Sequelize-ORM", "main": "server.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [ "Nodejs", "Express", "JWT", "Sequelize", "PostgreSQL", "Authentication" ], "author": "grokonez.com", "license": "ISC", "dependencies": { "bcryptjs": "^2.4.3", "express": "^4.16.3", "jsonwebtoken": "^8.3.0", "pg": "^7.5.0", "pg-hstore": "^2.3.2", "sequelize": "^4.39.0" } } |
Create Sequelize Models
– User
model ->
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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 ->
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
const env = { database: 'testdb', 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
->
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
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 NOTisAdmin
-> checking an User hasADMIN
role or NOTisPmOrAdmin
-> checking an User hasPM
orADMIN
role or NOT
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
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.map(role => role.toUpperCase()) } } }).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.
1 2 3 4 |
module.exports = { 'secret': 'grokonez-super-secret-key', ROLEs: ['USER', 'ADMIN', 'PM'] }; |
Server
– /app/server.js
file ->
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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 PostgreSQL database:
Sign Up
-> All Logs of Sign Up:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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); |
-> PostgreSQL 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: