Alexander Nnakwue Software engineer. React, Node.js, Python, and other developer tools and libraries.

JWT authentication from scratch with Vue.js and Node.js

17 min read 4976

JWT Authentication from Scratch with Vue.js and Node.js

Introduction

JWT, an acronym for JSON Web Token, is an open standard that allows developers to verify the authenticity of pieces of information called claims via a signature. This signature can either be a secret or a public/private key pair. Together with the header and the payload, they can be used to generate or construct a JWT, as we will get to see later.

JWTs are commonly used for authentication or to safely transmit information across different parties. Here’s a common flow for JWT-based authentication systems: once a user has logged into an app, a JWT is created on the server and returned back to the calling client.

Each subsequent request will include the JWT as an authorization header, allowing access to protected routes and resources. Also, once the backend server verifies the signature is valid, it extracts the user data from the token as required. Note that in order to ensure a JWT is valid, only the party holding the keys or secret is responsible for signing the information.

In this post, we will be focusing on using JWT to perform authentication requests on a Vue.js client app with a Node.js backend. But first, let’s review how JWT works in a nutshell.

How JWT authentication works

In JWT authentication-based systems, when a user successfully logs in using their credentials, a JSON Web Token will be returned back to the calling client. Whenever the user wants to access a protected route or resource, the user agent sends the same JWT, typically in the Authorization header using the Bearer schema.

The content of the header should look like this:

Authorization: Bearer <token>

For a user to be granted access to a protected resource, the server routes will have to check for the presence of a valid JWT in the Authorization header. As a bonus, sending JWTs in the Authorization header also solves some issues related to CORS. This applies even if the app is served from an entirely different domain.

Note: Even if JWTs are signed, the information is still exposed to users or other parties because the data are unencrypted. Therefore, users are encouraged not to include sensitive information like credentials within a JWT payload. Additionally, tokens should always have an expiry.

Bootstrapping our Node.js application

Like we said earlier, we are going to explore how JWT works by setting up a Vue.js application with JWT as a means of authenticating to a backend Node.js server. We will start by building out the backend part of the application, which handles both generating and subsequently verifying the JWT. The full code for this tutorial can be found in this GitHub repo.

To begin with, let’s crack open our terminal and install all the necessary dependencies for our app. We will be making use of the jsonwebtoken package on npm, and we’ll use express for our server. Also, we will be installing babel-polyfill, which provides the polyfills necessary for a full ES2015+ environment.

For a full list of the dependencies for our application, check the package.json file. The full contents of that file are shown below:

We made a custom demo for .
No really. Click here to check it out.

{
  "name": "jwt-vue-node-backend",
  "version": "1.0.0",
  "description": "Backend server for the JWT-Vue-Node-App",
  "main": "server.js",
  "scripts": {
    "create-dev-tables": "babel-node ./app/db/dbConnection createUserTable",
    "drop-dev-tables": "babel-node ./app/db/dbConnection dropUserTable",
    "dev": "nodemon --watch . --exec babel-node -- server/server",
    "start-dev": "babel-node server/server.js",
    "build-server": "babel server -d dist",
    "build": "rm -rf dist && mkdir dist && npm run build-server",
    "start-prod": "node dist/server.js",
    "start": "npm-run-all -p start-prod",
    "revamp": "npm-run-all -p dev drop-dev-tables"
  },
  "esModuleInterop": true,
  "keywords": [
    "Node",
    "Postgres"
  ],
  "author": "Alexander Nnakwue",
  "license": "MIT",
  "devDependencies": {
    "@babel/cli": "^7.12.1",
    "@babel/core": "^7.12.3",
    "@babel/node": "^7.12.10",
    "@babel/preset-env": "^7.12.1",
    "@babel/register": "^7.12.10",
    "babel-preset-es2015": "^6.24.1",
    "babel-watch": "^7.0.0",
    "eslint": "^7.11.0",
    "eslint-config-airbnb-base": "^14.2.0",
    "eslint-plugin-import": "^2.22.1",
    "nodemon": "^2.0.5"
  },
  "dependencies": {
    "@hapi/joi": "^17.1.1",
    "babel-plugin-add-module-exports": "^1.0.4",
    "babel-polyfill": "^6.26.0",
    "bcryptjs": "^2.4.3",
    "body-parser": "^1.19.0",
    "cors": "^2.8.5",
    "dotenv": "^8.2.0",
    "express": "^4.17.1",
    "jsonwebtoken": "^8.5.1",
    "make-runnable": "^1.3.8",
    "moment": "^2.29.1",
    "npm-run-all": "^4.1.5",
    "pg": "^8.4.1"
  },
  "engines": {
    "node": "13.7.0",
    "npm": "6.13.6"
  }
}

As we can see from the file above, we have installed a bunch of packages. Basically, we have installed packages needed to make Babel work as a development dependency. We began by making sure we have the Babel CLI globally installed. We also need to make sure we install other required dependencies for our application: eslint, nodemon, pg, etc., as shown above.

Pay special attention to the scripts section of the package.json file, which should show us how to create the user table, drop the user table, build the server file of our app, and run the app on our dev environment. This section also shows us how to run our app in production. Other details can be found in the aforementioned file.

At the end of the day, our folder structure should look like this:

Our App's Folder Structure
Folder structure for our completed app.

As you can see, the app is a monorepo, which will easily allow us to add the client folder, build, and output the build to a dist directory. Lastly, we add the build path to our server.js file, as we will see in a moment.

From the folder structure above, the app folder contains all the extra components needed for our backend to run successfully. It includes the controllers, the database handlers, helpers, the middleware folders, and so on. Again, for a complete view of the folder structure and to check out or clone the repo for our app, please have a look at the source code on GitHub.

As we mentioned earlier, the client folder contains everything that has to do with the user-facing part of the app. The server folder contains the server.js file, a basic Express server that powers our application. Let’s see what it looks like:

import express from 'express'
import 'babel-polyfill'
import cors from 'cors'
import usersRoute from '../app/route/user.js'
import path from 'path'

const __dirname = path.resolve();
const app = express()

// Add  cors middleware
app.use(cors())

// Add middleware for parsing JSON and urlencoded data
app.use(express.urlencoded({ extended: false }))
app.use(express.json())

// Serve static files from the Vue app
app.use(express.static(path.join(`${__dirname}/client/dist`)))

// The "catchall" handler: for any request that doesn't
// match one above, send back Vue's index.html file.
app.get('/*', (req, res) => {
    res.sendFile(path.join(`${__dirname}/client/dist/index.html`))
})

app.use('/api/v1', usersRoute)

app.get('/', (req, res) => {
    res.status(200).send('Welcome to the JWT authentication with Node and Vue.js tutorial')
})

// catch 404 and forward to error handler
app.use((req, res, next) => {
    const err = new Error('Not Found')
    console.log(err)
    err.status = 404
    res.send('Route not found')
    next(err)
})

const server = app.listen(process.env.PORT || 3000).on('listening', () => {
    console.log(`App live and listening on port: ${process.env.PORT}` || 3000)
})

export default app

Before we proceed and explain what is going on in the server file above, we need to set up the Babel config for our application. The .babelrc file is shown below:

{
    "presets": ["@babel/preset-env"],
    "plugins": ["add-module-exports"]
}

The preset we’re using above lets us write the latest JavaScript syntax and eliminates the need to check which syntax transforms (and, optionally, browser polyfills) are needed by the OS target environment(s). The plugin allows Babel to understand and interpret our module.exports statements and transforms.

Now let’s quickly return to the server.js file above. As we can see, we are serving static files and assets from our client build path. We are also importing our routes from the route path. Then, lastly, we are importing the babel-polyfill package, which we explained above. Check out the repo for the eslint and other setups.

Before we delve into learning how everything fits together, let’s quickly set up our config file, which picks variables from our .env file. The contents of the file are shown below:

require('dotenv').config()
const { env } = process

module.exports = {
    name: env.APP_NAME,
    baseUrl: env.APP_BASE_URL,
    port: env.APP_PORT,
    databases: {
        postgres: {
            user: env.PG_USERNAME,
            password: env.PG_PASSWORD,
            host: env.PG_HOST,
            port: env.PG_PORT,
            db_name: env.PG_DATABASE,
            url: env.PG_URL,
        },
    },
}

As we can see, Postgres is our database of choice today. This means we need to provide the necessary details to connect to a live Postgres DB. Now let’s look at the most important part of the codebase, which is under the app folder. Basically, all the files located here are required to make our server respond to HTTP requests.

Let’s start with the user.js file in the routes folder. The content of the file is shown below:

const express = require('express')
const { registerUser, loginUser, me } = require('../controller/users.js')
const  {
 createUser,
} = require( '../helpers/validation.js')
const {
  decodeHeader,
 } = require( '../middleware/verifyAuth.js')

const router = express.Router()

// user Routes

router.post('/auth/signup', createUser, registerUser)
router.post('/auth/login', loginUser)
router.get('/me', decodeHeader, me)

module.exports = router

As we can see from the above route definition file, we need just three endpoints to demonstrate how JWT works with Vue and Node. Here, we can see that we are importing the controller file and the middleware file (.../middleware/verifyAuth.js), used to decode the Authorization header sent from the client. We’ll explore that in a moment.

For now, let’s look at the controller found in the user.js file via the ~/controller/users.js path. Let’s explore the login.js method as an example.

/**
   * login User
   * @param {object} req
   * @param {object} res
   * @returns {object} user object
   */

const loginUser = async (req, res) => {
    const { email, password } = req.body

    if (isEmpty(email) || isEmpty(password)) {
        return Response.sendErrorResponse({
            res,
            message: 'Email or Password detail is missing',
            statusCode: 400,
        })
    }

    if (!isValidEmail(email) || !validatePassword(password)) {
        return Response.sendErrorResponse({
            res,
            message: 'Please enter a valid Email or Password',
            statusCode: 400,
        })
    }

    const loginUserQuery = 'SELECT * FROM users WHERE email = $1'

    try {

        const { rows } = await dbQuery.query(loginUserQuery, [email])
        const dbResponse = rows[0]

        if (!dbResponse) {
            return Response.sendErrorResponse({
                res,
                message: 'User with this email does not exist',
                statusCode: 400,
            })
        }

        if (!comparePassword(dbResponse.password, password)) {
            return Response.sendErrorResponse({
                res,
                message: 'The password you provided is incorrect',
                statusCode: 400,
            })
        }

        const token = Utils.generateJWT(dbResponse)
        const refreshExpiry = moment().utc().add(3, 'days').endOf('day')
            .format('X')
        const refreshtoken = Utils.generateJWT({ exp: parseInt(refreshExpiry), data: dbResponse._id })
        delete dbResponse.password
        return Response.sendResponse({
            res,
            responseBody: { user: dbResponse, token, refresh: refreshtoken },
            message: 'login successful',
        })
    } catch (error) {
        console.log(error)
        return Response.sendErrorResponse({
            res,
            message: error,
            statusCode: 500,
        })
    }
}

After we finish with the usual password comparison, we generate a JWT with the details of the user returned from querying our pg database. Next, we generate a refresh token (with the refresh``E``xpiry, of course). Finally, we return the token and the refresh token as part of the response. The parts we are interested in are shown below:

const token = Utils.generateJWT(dbResponse)
const refreshExpiry = moment().utc().add(3, 'days').endOf('day')
            .format('X')
const refreshtoken = Utils.generateJWT({ exp: parseInt(refreshExpiry), data: dbResponse._id })

Note: I already set up a Postgres database with the Heroku Postgres add-on. We’ll discuss the details later, as we plan to deploy our app to the Heroku environment.

Now, let’s look at the Utils folder, where the generateJWT method is defined. Note that we import the Utils module at the top of the controller file on line 10. Navigating to the helpers/utils.js path, we will learn firsthand how to verify and sign tokens. Let’s look at the content of that file:

const bcrypt = require('bcryptjs')
const jwt = require('jsonwebtoken')
const fs = require('fs')

const privateKEY = fs.readFileSync('./private.key', 'utf8')
const publicKEY = fs.readFileSync('./public.key', 'utf8')

const i = 'jwt-node'
const s = 'jwt-node'
const a = 'jwt-node'

const verifyOptions = {
    issuer: i,
    subject: s,
    audience: a,
    expiresIn: '8784h',
    algorithm: ['RS256'],
}

const saltRounds = 10

const salt = bcrypt.genSaltSync(saltRounds)

    const generateJWT = (payload) => {
        const signOptions = {
            issuer: i,
            subject: s,
            audience: a,
            expiresIn: '8784h',
            algorithm: 'RS256',
        }

        const options = signOptions
        if (payload && payload.exp) {
            delete options.expiresIn
        }
        return jwt.sign(payload, privateKEY, options)
    }

    const verifyJWT = (payload) => {
        return jwt.verify(payload, publicKEY, verifyOptions)
    }

    const hashPassword = (password) => {
        const hash = bcrypt.hashSync(password, salt)
        return hash
    }

module.exports = {
    hashPassword, verifyJWT, generateJWT
}

Note: The generateJWT method synchronously signs the given payload into a JSON Web Token string by making use of a privateKEY and an options argument.

As we can see from the code snippet above, we have two major methods we are interested in: verifyJWT and generateJWT. Let’s explore the latter method, which follows the JWT creation approach, constructed from three different elements: the header, the payload, and the signature/encryption data separated by dots (.).

JWT structure

As we just mentioned, JWTs are constructed from three different elements, including the header, the payload, and the signature.

The header is a JSON object which contains claims about itself including the type of token. It also consist of the signing algorithm being used, such as HMAC, SHA-256 with RSA, or ES256. Here, we are using the RS256 algorithm. It also defines whether the JWT is signed or encrypted.

const signOptions = {
            issuer: i,
            subject: s,
            audience: a,
            expiresIn: '8784h',
            algorithm: 'RS256',
            "typ": "JWT"
        }

The second part of the token is the payload, which usually contains details about the user. It may also contain some claims, which are statements about that user.

Note: There are three types of claims. They include registered, public, and private claims. Some of these claims are: iss (issuer), exp (expiration time), sub (subject), aud (audience), and so on.

The structure of a sample payload is shown below:

{
  id: 2,
  email: '[email protected]',
  firstname: 'Kelechi',
  lastname: 'Oti',
  password: '$2a$10$yK1bInjD2KieruxwFE9UV.ZuYiwZHu8ciX2vMZmLSvbrOdZ4/Maju',
  createdon: 2020-10-31T23:00:00.000Z
}

Note: For signed tokens, this information — though protected against tampering — is readable by anyone. So, ensure that you don’t put secret information in the payload or header elements of a JWT unless it is encrypted.

The third part is the signature, which is used to verify the message wasn’t changed along the way, and in the case of tokens signed with a private key, it can also verify that the sender of the JWT is who it says it is.

The signature is hugely dependent on the algorithm used for signing or encryption; in the case of unencrypted JWTs, there is none. So in essence, for unencrypted JWTs, the header is shown below:

{
    "alg": "none"
}

Here our JWT is encrypted and signed by a private/public key pair available in the root path of our repo.

Let’s look at a sample JWT below. Notice the dots separating the three elements of the JWT — the header, the payload, and the signature, respectively — which are base64url-encoded:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwiZW1haWwiOiJhbGV4Lm5uYWt3dWVAZ21haWwuY29tIiwiZmlyc3RuYW1lIjoiS2VsZWNoaSIsImxhc3RuYW1lIjoiT3RpIiwicGFzc3dvcmQiOiIkMmEkMTAkeUsxYkluakQyS2llcnV4d0ZFOVVWLlp1WWl3Wkh1OGNpWDJ2TVptTFN2YnJPZFo0L01hanUiLCJjcmVhdGVkb24iOiIyMDIwLTExLTAxVDAwOjAwOjAwLjAwMFoiLCJpYXQiOjE2MDgyNjY5MDYsImV4cCI6MTYzOTg4OTMwNiwiYXVkIjoicXVlc3Rpb25zLWdhbWUiLCJpc3MiOiJxdWVzdGlvbnMtZ2FtZSIsInN1YiI6InF1ZXN0aW9ucy1nYW1lIn0.FLXTabVVpZ_zuPEspoQe8U5kr8CwKNFKgXBwgQN6yKkW2RXmQGXyuML3dLy4XOEVEgaNoAjSI8De65CKlAq_dA

Note: JWT.io is an interactive playground for learning more about JWTs. For example, we can go ahead and copy the token from above and see what happens when we edit it.

Next, let’s look at the verifyJWT method, which basically allows us to make use of the publicKEY to verify that the privateKEY was responsible for signing the header and the payload.

A snippet of the verifyJWT method is shown below:

const verifyJWT = (payload) => {
        return jwt.verify(payload, publicKEY, verifyOptions)
}

Note: JWT has a verify method that synchronously verifies a given token, using a secret or a public key and options for the verification. Once the token is verified, a decoded value of that token is returned.

Now, when we log in to the app, we can see the user object, the token, and the refresh token, as shown below:

{
    "data": {
        "user": {
            "id": 2,
            "email": "[email protected]",
            "firstname": "Kelechi",
            "lastname": "Oti",
            "createdon": "2020-10-31T23:00:00.000Z"
        },
        "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwiZW1haWwiOiJhbGV4Lm5uYWt3dWVAZ21haWwuY29tIiwiZmlyc3RuYW1lIjoiS2VsZWNoaSIsImxhc3RuYW1lIjoiT3RpIiwicGFzc3dvcmQiOiIkMmEkMTAkeUsxYkluakQyS2llcnV4d0ZFOVVWLlp1WWl3Wkh1OGNpWDJ2TVptTFN2YnJPZFo0L01hanUiLCJjcmVhdGVkb24iOiIyMDIwLTEwLTMxVDIzOjAwOjAwLjAwMFoiLCJpYXQiOjE2MDgyNzMxNTgsImV4cCI6MTYzOTg5NTU1OCwiYXVkIjoiand0LW5vZGUiLCJpc3MiOiJqd3Qtbm9kZSIsInN1YiI6Imp3dC1ub2RlIn0.ky4EYUP3t10qaH1aCc0g_jxX5zt8U03l7lrwfpVjViYGqufxRcLoYNV8rw8SygxouXOqSPiF_LxLGv3fi4MQyQ",
        "refresh": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDg1OTUxOTksImlhdCI6MTYwODI3MzE1OCwiYXVkIjoiand0LW5vZGUiLCJpc3MiOiJqd3Qtbm9kZSIsInN1YiI6Imp3dC1ub2RlIn0.VvWQ17Wzyw-tJdRiIxsVzXNhl-nQSPf6Wo1iFOtC3BeueywB1lLaqOK4rmO0UAxKovx6sjZufu2mXEubKsiLOg"
    },
    "status": true,
    "message": "login successful"
}

Token and refresh token

Tokens give users access to protected resources. They are usually short-lived and may have an expiration date attached to their headers. They may also contain additional information about the user.

Refresh tokens, on the other hand, allow users request new tokens. For example, after a token has expired, a client may perform a request for a new token to be generated by the backend server. For this to happen, a refresh token is required. In contrast to access tokens, refresh tokens are usually long-lived.

Now let’s explore the verifyAuth file located in the middleware folder. This file contains middleware methods responsible for decoding the Authorization sent from the client and validating that the token is valid. In that file, we are interested in the decodeHeader method, which is shown below:

const decodeHeader = (req, res, next) => {
    let token = req.headers['x-access-token'] || req.headers.authorization || req.body.token
    if (!token) {
        return Response.sendErrorResponse({ res, message: 'No token provided', statusCode: 401 })
    }
    if (token.startsWith('Bearer ')) {
        // Remove Bearer from string
        token = token.slice(7, token.length)
        if (!token || token === '') Response.sendErrorResponse({ res, message: 'No token provided', statusCode: 401 })
    }
    // call the verifyJWT method to verify the token is valid
    const decoded = Utils.verifyJWT(token)
    if (!decoded) Response.sendErrorResponse({ res, message: 'invalid signature', statusCode: 403 })
    // attach the decoded token to the res.user object
    if (decoded) res.user = decoded
    res.token = token
    return next()
}

As we can see from the decodeHeader method above, we are accepting the token from the client in the form of authorization headers or in the req.body. If no token is provided, we are returning an error. Also, we are checking whether the token comes with the Bearer schema; if it does, we call the verifyJWT method from the Utils module.

This method, as we previously discussed, verifies the sent token from the client using the public key and returns the decoded value of the token. Note that if the decoded value is not returned, it means the initially signed token is invalid. Lastly, we append the decoded value to the response object.

Note: Since we are majorly focused on JWT authentication, we won’t be covering some parts of the app. For details about the database connections, for example, we can check the db folder located in the app directory. For other details about how things fit together as per the entire application, check the repo on GitHub.

Building our client-side Vue.js app

Now, let’s focus on the client-side part of the application. To bootstrap a Vue.js app, we can navigate to our terminal/command prompt and run the following command in the root folder: vue create <folder-name>.

To begin with, we should go ahead and install the Vue CLI globally on our machine if we have not done so previously. To do so, we can run npm install -g @vue/cli or yarn global add @vue/cli. We can also take look at the package.json file to get a feel for the dependencies we might need for the client app.

{
  "name": "vue-jwt-app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "heroku-postinstall": "npm install && npm run build"
  },
  "proxy": "http://localhost:3000",
  "dependencies": {
    "@babel/preset-env": "7.3.4",
    "axios": "^0.20.0",
    "bootstrap": "^4.5.3",
    "bootstrap-vue": "^2.18.0",
    "core-js": "^3.6.5",
    "vue": "^2.6.11",
    "vue-jwt-decode": "^0.1.0",
    "vue-router": "^3.4.7"
  },
  "devDependencies": {
    "@vue/babel-preset-app": "^4.5.8",
    "@vue/cli-plugin-babel": "^4.5.8",
    "@vue/cli-plugin-eslint": "^4.5.0",
    "@vue/cli-service": "^4.5.0",
    "babel-eslint": "^10.1.0",
    "eslint": "^6.7.2",
    "eslint-config-prettier": "^6.14.0",
    "eslint-plugin-prettier": "^3.1.4",
    "eslint-plugin-vue": "^6.2.2",
    "less": "^3.12.2",
    "less-loader": "^7.0.2",
    "vue-template-compiler": "^2.6.11"
  },
  "eslintConfig": {
    "root": true,
    "env": {
      "node": true
    },
    "extends": [
      "plugin:vue/essential",
      "plugin:prettier/recommended",
      "eslint:recommended"
    ],
    "parserOptions": {
      "parser": "babel-eslint"
    },
    "rules": {}
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead"
  ],
  "Author": "Alexander Nnakwue"
}

As we can see from the package.json file above, we have installed a couple of dependencies needed for our client app to run. Most importantly, we have installed vue-jwt-decode, which (as its name implies) is a JWT decoder for Vue.js applications. Also, we can see that we are using Axios to make API calls to our backend server. Other important dependencies needed for our client app to run can be found in the package.json file above.

Moving on, let’s navigate to the src/components/auth folder. There, we will find all the needed components for our client app. Here, we will be looking at the login component first. Why? Well, when a user logs in to our app, we save the generated JWT to local storage. Lets see how the login component looks:

<template>
  <div class="container">
    <b-card
      bg-variant="dark"
      header="Vue_JWT_APP"
      text-variant="white"
      class="text-center"
    >
      <b-card-text>Already have an account? Login Here!</b-card-text>
      <div class="row">
        <div class="col-lg-6 offset-lg-3 col-sm-10 offset-sm-1">
          <form
            class="text-center border border-primary p-5"
            style="margin-top:70px;height:auto;padding-top:100px !important;"
            @submit.prevent="loginUser"
          >
            <input
              type="text"
              id="email"
              class="form-control mb-5"
              placeholder="Email"
              v-model="login.email"
            />
            <!-- Password -->
            <input
              type="password"
              id="password"
              class="form-control mb-5"
              placeholder="Password"
              v-model="login.password"
            />
            <p>
              Dont have an account? Click
              <router-link to="/register"> here </router-link> to sign up
            </p>
            <!-- Sign in button -->
            <center>
              <button class="btn btn-primary btn-block w-75 my-4" type="submit">
                Sign in
              </button>
            </center>
          </form>
        </div>
      </div>
    </b-card>
  </div>
</template>
<script>
export default {
  data() {
    return {
      login: {
        email: "",
        password: ""
      }
    };
  },
  methods: {
    async loginUser() {
      try {
        let response = await this.$http.post("/auth/login", this.login);
        let token = response.data.data.token;
        localStorage.setItem("user", token);
        // navigate to a protected resource 
        this.$router.push("/me");
      } catch (err) {
        console.log(err.response);
      }
    }
  }
};
</script>

Note that we are making use of bootstrap-vue to style our components. As we can see above, once we log in to our app via the backend API, we are setting the returned token to localStorage. Once that happens, we are navigating to the Home component using the router push API.

Here, as a means of demonstrating the entire flow, we retrieve the token from local storage, decode said token using the vue-jwt-decode package, then finally append that to the user object, which we can then display in our Home component:

<template>
  <div>
    <b-navbar id="navbar" toggleable="md" type="dark" variant="info">
      <b-navbar-brand href="#">
        You are currently viewing the Vue_JWT_App
      </b-navbar-brand>
      <b-navbar-nav class="ml-auto">
        <b-nav-text>{{ user.firstname }} | </b-nav-text>
        <b-nav-item @click="logUserOut" active>Logout</b-nav-item>
      </b-navbar-nav>
    </b-navbar>
    <section>
      <div class="container mt-5">
        <div class="row">
          <div class="col-md-12">
            <ul class="list-group">
              <li class="list-group-item">
                Name : {{ user.firstname }} {{ user.lastname }}
              </li>
              <li class="list-group-item">Email : {{ user.email }}</li>
            </ul>
          </div>
        </div>
      </div>
    </section>
  </div>
</template>
<script>
import VueJwtDecode from "vue-jwt-decode";
export default {
  data() {
    return {
      user: {}
    };
  },
  methods: {
    getUserDetails() {
      // get token from localstorage
      let token = localStorage.getItem("user");
      try {
      //decode token here and attach to the user object
      let decoded = VueJwtDecode.decode(token);
      this.user = decoded;    
      } catch (error) {
        // return error in production env
        console.log(error, 'error from decoding token')
      }
    },
    logUserOut() {
      localStorage.removeItem("user");
      this.$router.push("/login");
    }
  },
  created() {
    this.getUserDetails();
  }
};
</script>
<style>
#navbar {
  margin-bottom: 15px;
}
</style>

Some things to note: because the vue-jwt-decode package is lightweight, we need to handle error cases on our end by wrapping it in a try/catch block. Additionally, details about the route file and other files needed for the entire client app setup can be found in the client folder here on GitHub.

Deploying our app to Heroku

To deploy our app to Heroku, we first need a Heroku account and the Heroku CLI installed on our dev machine. Visit this part of the documentation for instructions. After we’re done specifying settings like the Node engine version and the start script, we can then go ahead and test our app locally.

To do so, we need a Procfile in the root of our project. Heroku needs the Procfile to tell our app the path of the server to run. The content of the file is shown below:

web: node ./dist/server.js

To start our app locally, we can run, heroku local web. The output is shown below:

[OKAY] Loaded ENV .env File as KEY=VALUE Format
01:43:05 web.1   |  postgres://kdwqywwonzxzoa:8[email protected]ec2-54-156-149-189.compute-1.amazonaws.com:5432/daclachtll2s1g DATABASE_URL
01:43:05 web.1   |  3000 APP-PORT
01:43:05 web.1   |  App live and listening on port: 3000

As we can see, we have set up a Postgres add-on on the Heroku dashboard since we are using a Postgres database for our app. Additionally, looking at the script section of our package.json file, we can see the parts needed by our app for production.

The build command runs "rm -rf dist && mkdir dist && npm run build-server", which deleted any old dist folder, creates a dist directory, and builds our app. The build-server command runs "babel server -d dist". The final start script can be found in the script section of the same file as above.

Now all that’s left is to actually deploy our app to Heroku. To do so, we should ensure to commit our changes to Git ( on the master branch), then use the command heroku login in the Heroku CLI to log in to our Heroku account. Once we enter the correct credentials, we are good to create a new Heroku app with the heroku create command.

Once we are done, we can then push our app to the Heroku master branch by running, git push heroku master. We can then see the URL to our deployment, which we can find in a truncated portion of the output shown below:

remote:        found 0 vulnerabilities
remote:        
remote:        
remote: -----> Build succeeded!
remote: -----> Discovering process types
remote:        Default types for buildpack -> web
remote: 
remote: -----> Compressing...
remote:        Done: 36.8M
remote: -----> Launching...
remote:        Released v5
remote:        https://secure-cliffs-18654.herokuapp.com/ deployed to Heroku
remote: 
remote: Verifying deploy... done.
To https://git.heroku.com/secure-cliffs-18654.git
   91ced12..28baa4c  master -> master

The final step is to copy the URL https://secure-cliffs-18654.herokuapp.com/, navigate to the src/main.js folder of our client app, and update the base URL. The final content of that file is shown below:

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import axios from "axios";
import BootstrapVue from "bootstrap-vue";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";
const base = axios.create({
  baseURL: "https://secure-cliffs-18654.herokuapp.com/api/v1" // replace on production env
});
import store from "./store/index";
Vue.config.productionTip = false;
Vue.use(BootstrapVue);
Vue.prototype.$http = base;
new Vue({
  store,
  router,
  render: h => h(App)
}).$mount("#app");

Note the part where we have added the baseURL with the axios.create method. After this, we can commit these new changes again to Git, and we’re live. Finally, to test our app, we can navigate to the login route and test our deployment. The path is shown below:

https://secure-cliffs-18654.herokuapp.com/login

Once we register for a new account, we can then log in, and we’ll be redirected immediately to the home route:

Login Page for Our App
Our app’s login page.
Homepage for Our App
Our app’s homepage.

Once we are redirected to the homepage, we are logging the JWT payload of the currently authenticated user to the console just for demonstration purposes. The details are shown in the image below:

The JWT Payload for the Currently Authenticated User
JWT payload of the currently authenticated user.

Conclusion

In this tutorial, we have covered how to integrate JWT in a Node and Vue.js app. We started with the entire backend setup, built our app, and then worked on the client part of the application.

As we can see from the exercise, JWT offers a purely stateless authentication mechanism as the user state is never saved in the server memory or database. The server’s protected routes will check for a valid JWT in the Authorization header, and if there is, the user will be granted access.

We have learnt how JWT works, the structure of JWTs and how they can be verified. We have also learnt about how to generate JWTs and verify JWT data. On the client layer, we have learnt how to decode the JWT using the vue-jwt-decode library.

Finally, we should note that storing tokens in localStorage comes with its own set of risks since they are vulnerable to cross-site scripting (XSS) attacks. It’s essential to review the details about preventing security issues with JWTs and when you should use something else.

For more details on JWTs, refer to this detailed handbook and the documentation. Please reach out in the comment section for questions and feedback and thanks for reading along!

Experience your Vue apps exactly how a user does

Debugging Vue.js applications can be difficult, especially when there are dozens, if not hundreds of mutations during a user session. If you’re interested in monitoring and tracking Vue mutations for all of your users in production, try LogRocket. https://logrocket.com/signup/

LogRocket is like a DVR for web apps, recording literally everything that happens in your Vue apps including network requests, JavaScript errors, performance problems, and much more. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred.

The LogRocket Vuex plugin logs Vuex mutations to the LogRocket console, giving you context around what led to an error, and what state the application was in when an issue occurred.

Modernize how you debug your Vue apps - .

Alexander Nnakwue Software engineer. React, Node.js, Python, and other developer tools and libraries.

Leave a Reply