DEV Community

loading...

Nodejs JWT Authentication - Nodejs/Express RestAPIs + JSON Web Token + BCryptjs + Sequelize + MySQL

loizenai
Software Engineer - Founder at https://loizenai.com
・16 min read

https://grokonez.com/node-js/jwt/nodejs-jwt-authentication-nodejs-express-restapis-json-web-token-bcryptjs-sequelize-mysql

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:

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 ->

nodejs-jwt-authentication-express-bcryptjs-jsonwebtoken-sequelize-decoded-token

Overview

Demo

Project Structure

nodejs-jwt-authentication-express-bcryptjs-jsonwebtoken-sequelize-project-structure

  • config package defines MySQL Database Configuration, JWT Secret Key & User Roles.
  • model package defines Role & User Sequelize models. nodejs-jwt-authentication-express-bcryptjs-jsonwebtoken-sequelize-many-to-many-user-role
  • router package defines RestAPI URLs, verification functions for signup, & verification JWT token function for signin.
  • controller package defines proccesing functions for each RestAPIs declared in router 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, check password 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 ->

nodejs-jwt-authentication-express-bcryptjs-jsonwebtoken-sequelize-sign-up

Sign In ->

nodejs-jwt-authentication-express-bcryptjs-jsonwebtoken-sequelize-adam-sign-in

Access API Successfully ->

nodejs-jwt-authentication-express-bcryptjs-jsonwebtoken-sequelize-adam-access-user-role-api

Unauthorized Access ->

nodejs-jwt-authentication-express-bcryptjs-jsonwebtoken-sequelize-adam-can-NOT-access-PM-role-api

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 posted username or email is duplicated or NOT
    • checkRolesExisted -> checking the posted User Role 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 has ADMIN role or NOT
  • isPmOrAdmin -> checking an User has PM or ADMIN 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 defines jwt-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:

nodejs-jwt-authentication-express-bcryptjs-jsonwebtoken-sequelize-table-schema

nodejs-jwt-authentication-express-bcryptjs-jsonwebtoken-sequelize-roles-tables

Sign Up

nodejs-jwt-authentication-express-bcryptjs-jsonwebtoken-sequelize-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:

nodejs-jwt-authentication-express-bcryptjs-jsonwebtoken-sequelize-after-signup-data

SignIn and Access Protected Resources

  • Adam can access api/test/user url, can NOT access others.

-> Sign In:

nodejs-jwt-authentication-express-bcryptjs-jsonwebtoken-sequelize-adam-sign-in

-> Access Protected Resources:

nodejs-jwt-authentication-express-bcryptjs-jsonwebtoken-sequelize-adam-access-user-role-api

nodejs-jwt-authentication-express-bcryptjs-jsonwebtoken-sequelize-adam-can-NOT-access-PM-role-api

  • Jack can access api/test/user & api/test/pm url. Can NOT access /api/test/admin url.

-> Sign In:

nodejs-jwt-authentication-express-bcryptjs-jsonwebtoken-sequelize-jack-sign-in

-> Access Protected Resources:

nodejs-jwt-authentication-express-bcryptjs-jsonwebtoken-sequelize-JACK-can-access-user-role-api

nodejs-jwt-authentication-express-bcryptjs-jsonwebtoken-sequelize-JACK-can-access-PM-role-api

nodejs-jwt-authentication-express-bcryptjs-jsonwebtoken-sequelize-JACK-can-NOT-access-ADMIN-role-api

Thomas can access all URLs.

-> Sign In:

nodejs-jwt-authentication-express-bcryptjs-jsonwebtoken-sequelize-THOMAS-sign-in

-> Access Protected Resource:

nodejs-jwt-authentication-express-bcryptjs-jsonwebtoken-sequelize-THOMAS-can-access-ADMIN-role-api

SourceCode

Nodejs-JWT-Authentication

Discussion (0)

Forem Open with the Forem app