The tutorial is Part 2 of the series: Angular & Nodejs JWT Authentication fullstack | Nodejs/Express RestAPIs + JWT + BCryptjs + Sequelize + MySQL. Today we’re gonna build a Nodejs Authentication & Authorization RestAPIs that can interact with MySQL database.
– Part 1: Overview and Architecture.
– Part 3: Build Frontend
JWT Authentication with Nodejs/Express RestAPIs
Demo
Overview
HTTP request that matches route will be accepted by CORS Middleware
before coming to Security layer.
Security layer includes:
– JWT Authentication Middleware
: verify SignUp, verify token
– Authorization Middleware
: check User’s roles
Main Business Logic Processing
interacts with database via Sequelize
and send HTTP response (token, user information, data based on roles…) to client.
Config Middleware & RestAPIs
module.exports = function (app) { const controller = require('../controller/controller.js'); app.use(function (req, res, next) { res.header("Access-Control-Allow-Origin", "*"); res.header("Access-Control-Allow-Headers", "x-access-token, Origin, Content-Type, Accept"); next(); }); 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); }
– For HTTP Header, we allow x-access-token
for JWT.
– When a HTTP request call /signup
api, it will also be passed to checkDuplicateUserNameOrEmail()
and checkRolesExisted()
funtions before going to controller’s signup()
funtion.
– JWT Authentication middleware with verifyToken()
and role checking funtions (isPmOrAdmin
, isAdmin
) will be called before controller returns authorized data (based on roles).
Generate Token
Inside controller’s signin()
funtion, we use sign()
funtion from jsonwebtoken
:
var jwt = require('jsonwebtoken'); exports.signin = (req, res) => { User.findOne({ where: { username: req.body.username } }).then(user => { // check user & password... var token = jwt.sign({ id: user.id }, config.secret, { expiresIn: 86400 // expires in 24 hours }); // get other user information res.status(200).send({ auth: true, accessToken: token, username: user.username, authorities: authorities }); }); }
Verify Token
We get token from x-access-token
of HTTP headers, then use verify()
function of jsonwebtoken
:
const jwt = require('jsonwebtoken'); verifyToken = (req, res, next) => { let token = req.headers['x-access-token']; if (!token){ // notice that no token was provided...} jwt.verify(token, 'SECRET KEY', (err, decoded) => { if (err){ return res.status(500).send({ auth: false, message: 'Fail to Authentication. Error -> ' + err }); } req.userId = decoded.id; next(); }); }
User & Roles Relationship model
We define Role
& User
Sequelize models as below:
Implementation of the Many-to-Many relationship:
// user.model.js 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.js module.exports = (sequelize, Sequelize) => { const Role = sequelize.define('roles', { id: { type: Sequelize.INTEGER, primaryKey: true }, name: { type: Sequelize.STRING } }); return Role; } // db.config.js const Sequelize = require('sequelize'); const sequelize = new Sequelize(...); 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;
Nodejs server for JWT Authentication example Overview
Goal
The diagram below show how our system handles User Registration and User Login processes:
– /api/auth/signup
:
– /api/auth/signin
:
– /api/test/user
:
– /api/test/pm
:
– /api/test/admin
:
Technologies
– Nodejs/Express
– Json Web Token
– BCryptjs
– Sequelize
– MySQL
Project Structure
– config package defines MySQL Database Configuration, JWT Secret Key & User Roles.
– model package defines Role
& User
Sequelize models.
– router package defines RestAPI URLs, verification functions for signup
api, JWT verification for signin
api, and authorization functions for content requested by user roles.
– controller package defines process functions for each RestAPIs declared in router package.
Practice
Create Nodejs Project
Following the guide to creating a NodeJS/Express project.
Install Express, Sequelize, MySQL, Json Web Token, Bcryptjs:
$npm install express sequelize mysql2 jsonwebtoken bcryptjs --save
package.json
{ "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/user.model.js
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/role.model.js
module.exports = (sequelize, Sequelize) => { const Role = sequelize.define('roles', { id: { type: Sequelize.INTEGER, primaryKey: true }, name: { type: Sequelize.STRING } }); return Role; }
Sequelize Database Configuration
config/env.js
const env = { database: 'testdb', username: 'root', password: '123456', host: 'localhost', dialect: 'mysql', pool: { max: 5, min: 0, acquire: 30000, idle: 10000 } }; module.exports = env;
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;
More details at: Sequelize Many-to-Many association – NodeJS/Express, MySQL
Define RestAPIs Router with Middleware
RestAPIs Router
router/router.js
const verifySignUp = require('./verifySignUp'); const authJwt = require('./verifyJwtToken'); module.exports = function (app) { const controller = require('../controller/controller.js'); app.use(function (req, res, next) { res.header("Access-Control-Allow-Origin", "*"); res.header("Access-Control-Allow-Headers", "x-access-token, Origin, Content-Type, Accept"); next(); }); 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); }
Middleware functions
router/verifySignUp.js
const db = require('../config/db.config.js'); const config = require('../config/config.js'); const ROLEs = config.ROLEs; const User = db.user; 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;
router/verifyJwtToken.js
const jwt = require('jsonwebtoken'); const config = require('../config/config.js'); const db = require('../config/db.config.js'); 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 RestApis Controller
controller/controller.js
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 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({ message: 'Registered successfully!' }); }); }).catch(err => { res.status(500).send({ reason: err.message }); }); }).catch(err => { res.status(500).send({ reason: err.message }); }) } exports.signin = (req, res) => { User.findOne({ where: { username: req.body.username } }).then(user => { if (!user) { return res.status(404).send({ reason: '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 }); var authorities = []; user.getRoles().then(roles => { for (let i = 0; i < roles.length; i++) { authorities.push('ROLE_' + roles[i].name.toUpperCase()); } res.status(200).send({ auth: true, accessToken: token, username: user.username, authorities: authorities }); }) }).catch(err => { res.status(500).send({ reason: err.message }); }); } 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).send({ 'description': '>>> User Contents!', 'user': user }); }).catch(err => { res.status(500).send({ '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).send({ 'description': '>>> Admin Contents', 'user': user }); }).catch(err => { res.status(500).send({ '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).send({ 'description': '>>> Project Management Board', 'user': user }); }).catch(err => { res.status(500).send({ 'description': 'Can not access Management Board', 'error': err }); }) }We define
jwt-secret-key
& User Roles in config/config.js:module.exports = { 'secret': 'ozenero-super-secret-key', ROLEs: ['USER', 'ADMIN', 'PM'] };Server
server.js
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(); }); // 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: "PM" }); Role.create({ id: 3, name: "ADMIN" }); }SourceCode