https://ozenero.com/vue-js-spring-boot-example-spring-data-jpa-rest-mariadb-crud-example
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
{
"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:
- Code in-> 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
router.js
->app.post('/api/auth/signup', [verifySignUp.checkDuplicateUserNameOrEmail, verifySignUp.checkRolesExisted], controller.signup);
- SignIn Scenarios:
- Code in-> 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 returnedrouter.js
->app.post('/api/auth/signin', controller.signin);
- Access User Content:
- Code in-> Verify JWT Token -> If token is valid,
controller
will load & return User Info back to client -> Othercase, Error Code will be returnedrouter.js
->app.get('/api/test/user', [authJwt.verifyToken], controller.userContent);
- Access PM Content:
- Code in-> 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 returnedrouter.js
->app.get('/api/test/pm', [authJwt.verifyToken, authJwt.isPmOrAdmin], controller.managementBoard);
- Access Admin Content
- Code in-> 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 returnedrouter.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 NOT -
checkRolesExisted
-> 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; i 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 hasADMIN
role or NOT -
isPmOrAdmin
-> 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 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 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:
Top comments (1)
hi, thank you. I don't understand "user.getRoles()" ?