🔐 Structure of a single-page Vue 3 (TypeScript) app using JWT authorization requests to the backend
Tags: vue, typescript, tutorial, webdev
Published: 2021-11-15 16:37:49


## Introduction

Hello, DEV World! 👋

This weekend I had time to refactor my old projects, where I found an interesting case of working with JWT in a Vue.js web application, which I'll tell you about now.

> 🔥 **Disclaimer:** The article is intended primarily for **advanced** frontend developers, because it does **not** contain a beginner's description of technologies in use. You should **already** understand how the [Vue 3.x](https://v3.vuejs.org/guide/introduction.html) framework and [Vuex 4.x](https://next.vuex.vuejs.org/) works, and the basics of [JWT](https://jwt.io/introduction).

OK, let's go! 😉

<a name="toc"></a>
### 📝 Table of contents

- [A lyrical digression about the backend](#a-lyrical-digression-about-the-backend)
- [The main component of the Vue app](#the-main-component-of-the-vue-app)
  - [The structure of the business logic](#the-structure-of-the-business-logic)
  - [Function for updating tokens](#function-for-updating-tokens)
  - [Function for calling the background update tokens](#function-for-calling-the-background-update-tokens)
- [The result we got](#the-result-we-got)

<a name="#a-lyrical-digression-about-the-backend"></a>
## A lyrical digression about the backend

I believe that without a description of the backend scheme, it is impossible to understand my implementation in the frontend (**which is the purpose of this article**). Therefore, I will ask for some of your time to explain the decisions I made.

OK. So, I developed the backend as microservices, which was divided into the client's authorization micro-backend (since the project has been completely closed to anonymous users), and the REST API micro-backend for interaction with the project afterwards.

> 📌 **Note:** Examples of code for these microservices will be at [Go](https://golang.org/doc/) language and the [Fiber](https://gofiber.io/) web framework, since that's my main stack (at the moment).

Here's a simple diagram for a visual representation:

![simple diagram 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eltsto7h5yf16fkoxh2b.png)

And here are the specific implementations of tokens update [TokenRenew()](https://github.com/koddr/auth-komentory/blob/4efc68b6d3d033c55e03c705f7700a9c39419ab4/app/controllers/token_controller.go#L17-L135) and client authorization [UserLogin()](https://github.com/koddr/auth-komentory/blob/4efc68b6d3d033c55e03c705f7700a9c39419ab4/app/controllers/user_controller.go#L178-L279). 

> 👉 **Hey!** Full examples of micro-backends (auth and API) can be found [here](https://github.com/koddr/auth-komentory) and [here](https://github.com/koddr/auth-komentory). Don't be alarmed that this repository is marked as `DEPRECATED` by me. It's just an old version of one of my projects.

The main things to know about the backend in this case study: 

- The Fiber framework has built-in [middleware](https://docs.gofiber.io/api/middleware/encryptcookie) for encrypting cookies, which will generate an unreadable hash for the `refresh_token` each time.
- After successful user authorization (via `login` and `password`), the backend sends a JSON response with simple session information (JWT `access_token` and `expire` timestamp) and a special [HttpOnly](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) Cookie with an encrypted `refresh_token` for update JWT.
- As long as the JWT will be valid, for example by expiration time, the client can perform any operations with the private API methods (which require authorization).
- But if the client tries to make a request with an already expired JWT (or no JWT at all), the backend will behave as follows:
  - If the `refresh_token` in the `HttpOnly` Cookie is valid, then the backend will generate a new pair of access and refresh tokens and send it to the client;
  - If something went wrong, then the backend will send the HTTP 401 Unauthorized error to the client and skip connection;

> ⚡️ **Note:** This backend schema allows us to securely store the JWT session in web application memory (for example, I use [Vuex 4.x](https://next.vuex.vuejs.org/guide/) for this).

---

The interesting thing is that the end user will never be “disconnected” as long as he has a valid `refresh_token` in his cookies! Furthermore, not to worry that if the user refreshes the page or closes the browser tab, they will need to re-login when they return.

Endless session for your SPA here and now! 🎉

---

Great, I hope you now have a clearer picture of how the backend works, for which we will now write the frontend in Vue.js 3. _If anything remains unclear, please write about it in the comments._

[↑ Table of contents](#toc)

<a name="#the-main-component-of-the-vue-app"></a>
## The main component of the Vue app

And here's the part of the article we're all here for: implementation in a real Vue.js SPA web application. 

From the comments in my previous articles, I realized that the whole code listing is _quite difficult_ to understand. Therefore, I will break it into logical sections and describe them one by one in plain text format.

> 👉 **Hey!** A full code of this frontend part can be found [here](https://github.com/koddr/app-komentory) (still no need to worry about the `DEPRECATED` phrase).

So, this is what the `./src/App.vue` component contains:

```vue
<!-- ./src/App.vue -->

<template>
  <!-- 1️⃣ -->
  <router-view v-slot="{ Component, route }">
    <!-- 2️⃣ -->
    <transition name="fade" mode="out-in">
      <!-- 3️⃣ -->
      <component :is="Component" :key="route.path" />
    </transition>
  </router-view>
</template>

<!-- ... -->
```

The most common root Vue template for all the views in our application. And what happens in this code snippet:

1. Create a new root element `router-view` (read more about it [here](https://next.router.vuejs.org/api/#router-view-s-v-slot));
2. For smoother transitions, added `transition` element with `fade` effect (read more about it [here](https://next.router.vuejs.org/guide/advanced/transitions.html#per-route-transition));
3. Added the component itself with a unique key, which will be rendered in our view;

Now let's look at the business logic layer of our component. 

I will also divide it into three parts:

- The structure of the component;
- Function for updating tokens;
- Function for calling the background update tokens;

[↑ Table of contents](#toc)

<a name="#the-structure-of-the-business-logic"></a>
### The structure of the business logic

This is where the main magic will happen. Since this component is the main one for the whole SPA, it will have a built-in process for initially getting the token and updating it periodically.

> 💡 **By the way!** For imports, I use an alias `__/` that matches the settings of my [Vite](https://vitejs.dev/config/) config ([here](https://github.com/koddr/app-komentory/blob/82fadc20fbfdfc56cbc207a837aeb050a9e9d8a9/vite.config.ts#L8)).

```vue
<!-- ./src/App.vue -->

<!-- ... -->

<script lang="ts">

import { defineComponent, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'

import { useStore } from '__/store' // 1️⃣
import { 
  UPDATE_JWT, 
  UPDATE_CURRENT_USER, 
} from '__/store-constants' // 2️⃣
import { 
  TokenDataService as Token, 
  TokenResponse, 
} from '__/services' // 3️⃣

// ...

export default defineComponent({
  name: 'App',
  components: {
    // ...
  },
  setup: () => {
    // Define needed instances.
    const store = useStore()
    const router = useRouter()

    // Define needed states from the Vuex store.
    const { access_token, expire } = store.state.jwt

    // Define function for renew token.
    const tokenRenew = async () => {
      // ...will be described below...
    }

    // Define background async setInterval function for renew token.
    const tokenRenewTimer = setInterval(async () => {
      // ...will be described below...
    })

    // 4️⃣
    if (access_token === '' && expire === 0) tokenRenew()

    // 5️⃣
    onMounted(() => tokenRenewTimer) 
    onUnmounted(() => clearInterval(tokenRenewTimer))
  },
})

</script>
```

Perfect! Now let's review the important points:

1. I use a custom Vuex implementation of the state store adapted to TypeScript. So, I import my implementation of the `useStore` hook instead of the standard one (you can read more about it [here](https://next.vuex.vuejs.org/guide/typescript-support.html#simplifying-usestore-usage)).
2. For more convenient work with the Vuex store, I applied [constants for mutation types](https://next.vuex.vuejs.org/guide/mutations.html#using-constants-for-mutation-types).
3. Since SPA usually has numerous HTTP calls to the API, I usually write some services which are a better wrapper over the `axios` [instance](https://github.com/axios/axios#creating-an-instance) (with additional header settings). This helps simplify code separation for a particular business logic. In this case, for token renew requests (see example [here](https://github.com/koddr/app-komentory/blob/main/src/services/TokenDataService.ts)). 
4. If token and expire time not set, try to renew. This condition allows you to run the token update process if a JWT session has been deleted from the application memory (from Vuex store in this case).
5. Define needed lifecycle hooks with `tokenRenewTimer()` function. Subscribe to the periodic background token renew process when this component has been mounted, and clear timer after unmounted.

[↑ Table of contents](#toc)

<a name="#function-for-updating-tokens"></a>
### Function for updating tokens

This async function will do the basic work of retrieving the JWT session if the user has a valid `refresh_token` cookie.

```ts
// ...

// Define function for renew token.
const tokenRenew = async () => {
  try {
    const { data: token_response }: TokenResponse = await Token.renew(access_token)
    // Successful response from Auth server.
    if (token_response.status === 200) {
        // 1️⃣
        store.commit(UPDATE_JWT, token_response.jwt)
        store.commit(UPDATE_CURRENT_USER, token_response.user)
        // 2️⃣
        localStorage.setItem('_myapp', Math.random().toString(36).substring(2, 36))
    } else if (token_response.status === 401) {
        // Failed response from Auth server.
        const { name: current_route } = router.currentRoute.value // 3️⃣
        // 4️⃣
        if (current_route !== 'register') router.push({ name: 'login' })
    } else console.warn(token_response.msg)
  } catch (error: any) {
    console.error(error) // 5️⃣
  }
}

// ...
```

Not complicated, is it? Let's go into more detail:

1. Save the response data (a new JWT and user info) to the Vuex store.
2. Add a random string to the `localStorage` to indicate that the user has been authenticated. This marker is only needed to reduce the number of requests to the authorization server if the user has been successfully authorized.
3. Get current route name from `vue-router`.
4. Skip redirect, if current route name is `register`. This is important because it prevents forced redirects to the login page if the user goes through the registration process.
5. Show any other errors.

[↑ Table of contents](#toc)

<a name="#function-for-calling-the-background-update-tokens"></a>
### Function for calling the background update tokens

We got to the heart of a good UX of our app. This is the function that will, in the background, send periodic requests to the authorization server and get a new session (JWT + user info).

```ts
// ...

// Define background async setInterval function for renew token.
const tokenRenewTimer = setInterval(async () => {
  let now = new Date() // get current date
  let expire_time = new Date(expire * 1000 - 60000) // 1️⃣
  // 2️⃣
  if (expire_time <= now && '_myapp' in localStorage) await tokenRenew()
}, 60000) // 3️⃣

// ...
```

And that's what we're doing here:

1. Subtract 1 minute from JWT `expire` field.
2. If expire time is less or equal than now, and `localStorage` has `_myapp` item, then send request to renew token. 
3. Set 1 minute interval to make the periodic request.

[↑ Table of contents](#toc)

<a name="#the-result-we-got"></a>
## The result we got

As a result, we have a stable enough frame to implement any further web application logic. Now, you can make a getter in the store (like [this](https://github.com/koddr/app-komentory/blob/82fadc20fbfdfc56cbc207a837aeb050a9e9d8a9/src/store.ts#L69-L74) one) to check the actual state of user authorization in your other components.

Well, the article is in the style of a code review, which is even better than a _dry_ description of the sequence of actions. 

**What a great thing that happened!** 😎

[↑ Table of contents](#toc)

<a name=""></a>
## Photos and videos by

- Vic Shóstak https://shostak.dev

<a name=""></a>
## P.S.

If you want more articles (like this) on this blog, then post a comment below and subscribe to me. Thanks! 😻

And of course, you can help me make developers' lives even better! Just connect to one of my projects as a contributor. It's easy!

My projects that need your help (and stars) 👇

- 🔥 [gowebly][gowebly_url]: A next-generation CLI tool for easily build amazing web applications with Go on the backend, using htmx & hyperscript and the most popular atomic/utility-first CSS frameworks on the frontend.
- ✨ [create-go-app][cgapp_url]: Create a new production-ready project with Go backend, frontend and deploy automation by running one CLI command.
- 🏃 [yatr][yatr_url]: Yet Another Task Runner allows you to organize and automate your routine operations that you normally do in Makefile (or else) for each project.
- 📚 [gosl][gosl_url]: The Go Snippet Library provides snippets collection for working with routine operations in your Go programs with a super user-friendly API and the most efficient performance.
- 🏄‍♂️ [csv2api][csv2api_url]: The parser reads the CSV file with the raw data, filters the records, identifies fields to be changed, and sends a request to update the data to the specified endpoint of your REST API.
- 🚴 [json2csv][json2csv_url]: The parser can read given folder with JSON files, filtering and qualifying input data with intent & stop words dictionaries and save results to CSV files by given chunk size.

<!-- Footer links -->

[gowebly_url]: https://github.com/gowebly
[cgapp_url]: https://github.com/create-go-app
[yatr_url]: https://github.com/koddr/yatr
[gosl_url]: https://github.com/koddr/gosl
[csv2api_url]: https://github.com/koddr/csv2api
[json2csv_url]: https://github.com/koddr/json2csv