DEV Community

Cover image for JWT Authorization and Authentication, Node, Express, and Vue
Kevin Odongo
Kevin Odongo

Posted on

JWT Authorization and Authentication, Node, Express, and Vue

In this tutorial, we are going to add authentication and authorizations to our blog application that we have been using in our previous tutorials. We are going to use two options Passport or JWT. In today's will handle JWT.

Funny thing I also read other people's articles and what's interesting will always save to my reading list. This article explains extensible about JWT. I don't need to add more to it.

https://dev.to/kmistele/demystifying-jwt-how-to-secure-your-next-web-app-9h0.

As of me during my learning, I'm always curious to know how to implement what I have read practically to an application.

What we have learned so far:

  1. The approach of MongoDB, Express, Vue, and Node still falls under serverless because we are currently running our application without managing the infrastructure. Atlas is managing everything for us.

  2. The approach of AWS PostgreSQL, Express, Vue, and Node falls under the server approach because we are currently running our application in an EC2. We will have to manage the infrastructure.

  3. The approach of AWS MySQL, Express, Vue, and Node falls under the server approach because we are currently running our application in an EC2. We will have to manage the infrastructure.

For a startup, maintaining servers will be quite costly and therefore a consideration between Containers and serverless would be ideal.

Let us add the following components to the authentication directory in the front-end.

Login.vue

Alt Text

Signup.vue

Alt Text

Confirm.vue

Alt Text

In the end, we only want to allow GET articles API for public viewing of articles. The rest of the CRUD actions will only be allowed after authentication.

Let us begin.

Backend

If you followed our previous tutorial then currently you know we have two applications backend running on Node, Express, Mongo DB OR PostgreSQL OR MySQL depending on the database you selected.

We want to add authentication and authorization to this application. We will assume as follows.

  1. We want to allow only GET for the following routes to the public.
  2. We want to allow DELETE to the admin role everything else can be assessed by admin or user.

Below is how our blog.js in the routes folder will be

const express = require("express")
const router = express.Router()
const blog = require("../controller/blog.controller");
const { auth_jwt_token } = require("../authentication");

// /api/blog: GET, POST, DELETE
// /api/blog/:id: GET, PUT, DELETE
// /api/blog/published: GET

// Create a new blog
router.post("/", [auth_jwt_token.verifyToken], blog.create);

// Retrieve all blog
router.get("/", blog.findAll);

// Retrieve all published blog
router.get("/published", blog.findAllPublished);

// Retrieve a single blog with id
router.get("/:id", blog.findOne);

// Update a blog with id
router.put("/:id", [auth_jwt_token.verifyToken], blog.update);

// Delete a blog with id
router.delete("/:id", [auth_jwt_token.verifyToken, auth_jwt_token.isAdmin], blog.delete);

// Create a new blog
router.delete("/", [auth_jwt_token.verifyToken, auth_jwt_token.isAdmin], blog.deleteAll);

module.exports = router
Enter fullscreen mode Exit fullscreen mode

We will need two roles in our blog. A user and admin.
For token, you can use either jsonwebtoken or express.jwt. Let us install bcryptjs to hash our password and jsonwebtoken for our token.

yarn add jsonwebtoken bcryptjs
Enter fullscreen mode Exit fullscreen mode

index.js

In the index.js file when our backend application loads we would want to check our database if the roles have been set correctly, if empty we need to create the roles. Let us have an initialization function to handle checking on the roles.

const Role = db.role // reference the Role DB
function initialize() {
    Role.estimatedDocumentCount((err, count) => {
      if (!err && count === 0) {
        new Role({
          name: "user"
        }).save(err => {
          if (err) {
            console.log("error", err);
          }
          console.log("added 'user' to roles collection");
        });

        new Role({
          name: "admin"
        }).save(err => {
          if (err) {
            console.log("error", err);
          }  
          console.log("added 'admin' to roles collection");
        });
      }
    });
  }
Enter fullscreen mode Exit fullscreen mode

Below is the result when you first run your backend. We did not have the roles and it initialized the roles table and added the new roles.

Alt Text

routes

We are going to add a new route called auth.js in the routes folder and update the blog routes.

auth.js
This route will handle two functions signup and signin. The function verify_user_email assists in checking if the email has been registered before to avoid duplication.

const { verify_user_email } = require("../authentication");
const express = require("express")
const router = express.Router()
const auth = require("../controller/auth.controller");

router.post("/signin", auth.signin);

router.post("/signup", 
  [
      verify_user_email.checkDuplicateUsernameOrEmail,
      verify_user_email.checkRolesExisted
  ],
  auth.signup
)

module.exports = router
Enter fullscreen mode Exit fullscreen mode

blog.js
I have shared above how our blog.js routes folder should be.

That is all we have to do in the routes folder. Next, we need to update the index.js file and import our routes. With express.js you can load application-level and router-level middleware with an optional mount path. You can also load a series of middleware functions together, which creates a sub-stack of the middleware system at a mount point.

index.js

// routes
const blog = require('./app/routes/blog') // blog routes
const auth = require('./app/routes/auth') // user authentication

app.use('/api/blog',blog, function(req, res, next){
  res.header(
    "Access-Control-Allow-Headers",
    "x-access-token, Origin, Content-Type, Accept"
  );
  next();
}) // user authorization
app.use('/api/auth', auth, function(req, res, next){
  res.header(
    "Access-Control-Allow-Headers",
    "x-access-token, Origin, Content-Type, Accept"
  );
  next();
}) // auth authentication
Enter fullscreen mode Exit fullscreen mode

I hope we are on the same page at this stage. Keep close

Schema

Let us define the schema for the user and the roles. This will be done in the model's folder where we had the blog schema too.

role.model.js
Our roles will have a name and an id.

module.exports = mongoose => {
    const Role = mongoose.model(
      "Role",
      mongoose.Schema(
        {
          name: String,
        },
        { timestamps: true }
      ) 
    );
    return Role;
  };
Enter fullscreen mode Exit fullscreen mode

user.model.js
In the user model, we want to add username, email, password, and roles for the user. By default, the user will have a user role and upgraded to admin thereafter.
Note we have referenced the Role so we can get the correct id for the roles.

module.exports = mongoose => {
    const User = mongoose.model(
      "User",
      mongoose.Schema(
        {
          username: String,
          email: String,
          password: String,
          roles: [
            {
              type: mongoose.Schema.Types.ObjectId,
              ref: "Role"
            }
          ]
        },
        { timestamps: true }
      )
    );
    return User;
  };
Enter fullscreen mode Exit fullscreen mode

controller

In the controller folder let us add a controller that will handle authentication

auth.controller.js
The signup function will create a new user while the signing function will confirm the user exists. Then the user payload will be signed by the private key and a token will be generated. To verify the token we can verify the signature and decode with jwt or just decode jwt token. We will handle both scenarios.

const crypto = require('crypto');
const db = require("../models");
const User = db.user;
const Role = db.role;

var jwt = require("jsonwebtoken");
var bcrypt = require("bcryptjs");

exports.signup = (req, res) => {
  const user = new User({
    username: req.body.username,
    email: req.body.email,
    password: bcrypt.hashSync(req.body.password, 8)
  });

  user.save((err, user) => {
    if (err) {
      res.status(500).send({ message: err });
      return;
    }

    if (req.body.roles) {
      Role.find(
        {
          name: { $in: req.body.roles }
        },
        (err, roles) => {
          if (err) {
            res.status(500).send({ message: err });
            return;
          }

          user.roles = roles.map(role => role._id);
          user.save(err => {
            if (err) {
              res.status(500).send({ message: err });
              return;
            }

            res.send({ message: "User was registered successfully!" });
          });
        }
      );
    } else {
      Role.findOne({ name: "user" }, (err, role) => {
        if (err) {
          res.status(500).send({ message: err });
          return;
        }

        user.roles = [role._id];
        user.save(err => {
          if (err) {
            res.status(500).send({ message: err });
            return;
          }

          res.send({ message: "User was registered successfully!" });
        });
      });
    }
  });
};

exports.signin = (req, res) => {
  User.findOne({
    username: req.body.username
  })
    .populate("roles", "-__v")
    .exec((err, user) => {

      if (err) {
        res.status(500).send({ message: err });
        return;
      }

      if (!user) {
        return res.status(404).send({ message: "User Not found." });
      }

      var passwordIsValid = bcrypt.compareSync(
        req.body.password,
        user.password
      );

      if (!passwordIsValid) {
        return res.status(401).send({
          accessToken: null,
          message: "Invalid Password!"
        });
      }

      const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', {
        namedCurve: 'sect239k1'
      });

      // generate a signature of the payload
      const sign = crypto.createSign('SHA256');
      sign.write(`${user}`);
      sign.end();
      var signature = sign.sign(privateKey, 'hex');
      console.log(signature)


      // sign username
      var token = jwt.sign({ id: user.id }, signature, {
        expiresIn: 86400 // 24 hours
      });

      var authorities = [];

      for (let i = 0; i < user.roles.length; i++) {
        authorities.push("ROLE_" + user.roles[i].name.toUpperCase());
      }
      res.status(200).send({
        id: user._id,
        username: user.username,
        email: user.email,
        roles: authorities,
        accessToken: token, // access token
        signature: signature // signature
      });
    });
};
Enter fullscreen mode Exit fullscreen mode

Finally, let us create an authentication folder in the app folder.

touch /app/authentication
Enter fullscreen mode Exit fullscreen mode

Then create three files index.js, auth.js, and verify.js. verify.js will handle the verification of the user email while the auth.js will handle the verification of the user token and if the user is an admin.

auth.js

const jwt = require("jsonwebtoken");
const db = require("../models");
const User = db.user;
const Role = db.role;

verifyToken = (req, res, next) => {
  let token = req.headers["x-access-token"];
  let secret = req.headers["x-access-signature"];

  if (!token) {
    return res.status(403).send({ message: "No token provided!" });
  }


  // Prints: true
  jwt.verify(token, secret, (err, decoded) => {
    if (err) {
      return res.status(401).send({ message: "Unauthorized!" });
    }
    req.userId = decoded.id;
    next();
  });
};

isAdmin = (req, res, next) => {
  User.findById(req.userId).exec((err, user) => {
    if (err) {
      res.status(500).send({ message: err });
      return;
    }

    Role.find(
      {
        _id: { $in: user.roles }
      },
      (err, roles) => {
        if (err) {
          res.status(500).send({ message: err });
          return;
        }

        for (let i = 0; i < roles.length; i++) {
          if (roles[i].name === "admin") {
            next();
            return;
          }
        }

        res.status(403).send({ message: "Require Admin Role!" });
        return;
      }
    );
  });
};

const authJwt = {
  verifyToken,
  isAdmin,
};
module.exports = authJwt;
Enter fullscreen mode Exit fullscreen mode

verify.js

const db = require("../models");
const ROLES = db.ROLES;
const User = db.user;

checkDuplicateUsernameOrEmail = (req, res, next) => {
  // Username
  User.findOne({
    username: req.body.username
  }).exec((err, user) => {
    if (err) {
      res.status(500).send({ message: err });
      return;
    }

    if (user) {
      res.status(400).send({ message: "Failed! Username is already in use!" });
      return;
    }

    // Email
    User.findOne({
      email: req.body.email
    }).exec((err, user) => {
      if (err) {
        res.status(500).send({ message: err });
        return;
      }

      if (user) {
        res.status(400).send({ message: "Failed! Email is already in use!" });
        return;
      }

      next();
    });
  });
};

checkRolesExisted = (req, res, next) => {
  if (req.body.roles) {
    for (let i = 0; i < req.body.roles.length; i++) {
      if (!ROLES.includes(req.body.roles[i])) {
        res.status(400).send({
          message: `Failed! Role ${req.body.roles[i]} does not exist!`
        });
        return;
      }
    }
  }

  next();
};

const verifySignUp = {
  checkDuplicateUsernameOrEmail,
  checkRolesExisted
};

module.exports = verifySignUp;
Enter fullscreen mode Exit fullscreen mode

Export all the contents in the authentication file through the index.js in the folder.

index.js

const auth_jwt_token = require("./auth");
const verify_user_email = require("./verify");

module.exports = {
  auth_jwt_token,
  verify_user_email
};
Enter fullscreen mode Exit fullscreen mode

Let us test our backend and ensure we have configured everything correctly. I will be using Postman to test the following:

  1. Sign up a user with no admin access.
  2. Sign in a user.
  3. Create a blog (Token will be required)
  4. Delete a blog (Token and admin access will be required)

Front End

Let us configure the front end and link the communication between the two. Let us create a file called auth.script.js in the components directory.

import axios from "axios";

export const signup = async item => {
  let data = {
    username: item.username,
    email: item.email,
    password: item.password,
    roles: ["user"]
  };
  let request = {
    url: "http://localhost:3000/api/auth/signup", // should be replaced after going to production with domain url
    method: "post",
    headers: {
      "Content-type": "application/json"
    },
    data: JSON.stringify(data)
  };

  const response = await axios(request);
  return response;
};

export const login = async item => {
  let data = {
    username: item.username,
    password: item.password
  };
  let request = {
    url: "http://localhost:3000/api/auth/signin", // should be replaced after going to production with domain url
    method: "post",
    headers: {
      "Content-type": "application/json"
    },
    data: JSON.stringify(data)
  };

  const response = await axios(request);
  return response;
};

Enter fullscreen mode Exit fullscreen mode

On sign-in success, we need to ensure we save the user details safely. Here is an article about how to securely save your payloads securely. https://dev.to/gkoniaris/how-to-securely-store-jwt-tokens-51cf.

The logout function should clear the storage and redirect the user back to the login page or home page.

Sign up

In the sign-up component add the following function in your method section and call it on user submit.

// import the signup function from auth.script.js

// sibmit signup
    async submit() {
      this.loading = true;
      const response = await signup(this.item);
      if (response === "User was registered successfully!") {
        // DO NOT USE LOCAL STORAGE
        localStorage.setItem("user", JSON.stringify(response.data));
        this.item = {
          username: "",
          email: "",
          password: "",
          roles: ["user"]
        };
        this.loading = false;
        this.$router.push("/dashboard");
      } else {
        // error
        console.log("Error", response);
        setTimeout(() => {
          this.loading = false;
        }, 1000);
      }
    }
Enter fullscreen mode Exit fullscreen mode

Login

In the login component add the following function in your method section and call it on user submit.

// import the login function from auth.script.js

// sibmit login
    async submit() {
      this.loading = true;
      const response = await login(this.item);
      if (response.data.accessToken) {
         // DO NOT USE LOCAL STORAGE
        localStorage.setItem("user", JSON.stringify(response.data));
        this.item = {
          username: "",
          password: ""
        };
        this.loading = false;
        this.$router.push("/dashboard");
      } else {
        // error
        console.log("Error", response);
      }
    }
Enter fullscreen mode Exit fullscreen mode

log out

In the dashboard update the logout function by adding a way of clearing the user information you had saved.

// DO NOT USE LOCAL STORAGE
localStorage.removeItem("user")
Enter fullscreen mode Exit fullscreen mode

NOTE

For every request in the blog routes that requires authentication ensure your header contains the following:

headers: {
      "Content-type": "application/json",
      'x-access-token': item.accessToken,
      'x-access-signature': item.signature
    },
Enter fullscreen mode Exit fullscreen mode

Finally in your router guard all the routes in your vue application. In the router folder ensure you update the index.js file as follows.

const router = new VueRouter({
  routes: [
    {
      path: '/dashboard',
      component: Dashboard,
      // save you have a means of updating isAuthenticated
      beforeEach((to, from, next) => {
         if (to.name !== 'Login' && !isAuthenticated) next({ name: 
           'Login' })
          else next()
      })
    }
  ]
})
Enter fullscreen mode Exit fullscreen mode

Read more about protecting routes in Vue application https://router.vuejs.org/guide/advanced/navigation-guards.html#global-before-guards

MySQL

For MySQL, I will provide the files that need to be changed. Here is the article for MySQL https://dev.to/kevin_odongo35/aws-rds-mysql-express-vue-and-node-jfj

index.js

const Role = db.role // reference the Role DB
function initialize() {
  Role.create({
    id: 1,
    name: "user"
  });

  Role.create({
    id: 3,
    name: "admin"
  });
}
Enter fullscreen mode Exit fullscreen mode

role.model.js

module.exports = (sequelize, Sequelize) => {
    const Role = sequelize.define("roles", {
    id: {
      type: Sequelize.INTEGER,
      primaryKey: true
    },
    name: {
      type: Sequelize.STRING
    }
  });
  };
Enter fullscreen mode Exit fullscreen mode

user.model.js

const User = sequelize.define("users", {
    username: {
      type: Sequelize.STRING
    },
    email: {
      type: Sequelize.STRING
    },
    password: {
      type: Sequelize.STRING
    }
  });

  return User;
Enter fullscreen mode Exit fullscreen mode

/model/index.js

db.user = require("../models/user.model.js")(sequelize, Sequelize);
db.role = require("../models/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"
});

db.ROLES = ["user", "admin"];
Enter fullscreen mode Exit fullscreen mode

Everything else will stay as I have detailed above. Just edit the following files.

PostreSQL

For PostgreSQL, I will provide the files that need to be changed. Here is the article for PostgreSQL https://dev.to/kevin_odongo35/aws-rds-postgresql-express-vue-and-node-1k99

index.js

const Role = db.role // reference the Role DB
function initialize() {
  Role.create({
    id: 1,
    name: "user"
  });

  Role.create({
    id: 3,
    name: "admin"
  });
}
Enter fullscreen mode Exit fullscreen mode

role.model.js

module.exports = (sequelize, Sequelize) => {
    const Role = sequelize.define("roles", {
    id: {
      type: Sequelize.INTEGER,
      primaryKey: true
    },
    name: {
      type: Sequelize.STRING
    }
  });
  };
Enter fullscreen mode Exit fullscreen mode

user.model.js

const User = sequelize.define("users", {
    username: {
      type: Sequelize.STRING
    },
    email: {
      type: Sequelize.STRING
    },
    password: {
      type: Sequelize.STRING
    }
  });

  return User;
Enter fullscreen mode Exit fullscreen mode

/model/index.js

db.user = require("../models/user.model.js")(sequelize, Sequelize);
db.role = require("../models/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"
});

db.ROLES = ["user", "admin"];
Enter fullscreen mode Exit fullscreen mode

Everything else will stay as I have detailed above. Just edit the following files.

SENDING CODE FOR CONFIRMATION

You can implement this by using different products but I would recommend AWS SES. I once talked about how to configure AWS SES https://dev.to/kevin_odongo35/build-a-bulk-email-and-sms-app-with-vue-and-aws-ses-aws-sns-or-twilio-part-1-33jp. The whole logic I will detail in a course. From start to end. Keep close

I hope this tutorial has been helpful in how to implement JWT. I have provided articles to assist you in understanding more about JWT. How to securely to them.

Thank you

Top comments (3)

Collapse
 
bradtaniguchi profile image
Brad
// DO NOT USE LOCAL STORAGE
Enter fullscreen mode Exit fullscreen mode

So what should we use instead to save the JWT?

Collapse
 
kevin_odongo35 profile image
Kevin Odongo

I included this article to assist you in securing your JWT. George gave a good detailed explanation about it.

dev.to/gkoniaris/how-to-securely-s...

Collapse
 
samuelojes profile image
DGAME

God bless you for this article, I don't use Vue but I understand the logic and concept perfectly. Looking forward to your course.