DEV Community

Cover image for 🔐 Structure of a single-page Vue 3 (TypeScript) app using JWT authorization requests to the backend
Vic Shóstak
Vic Shóstak

Posted on

🔐 Structure of a single-page Vue 3 (TypeScript) app using JWT authorization requests to the backend

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 framework and Vuex 4.x works, and the basics of JWT.

OK, let's go! 😉

📝 Table of contents

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 language and the Fiber web framework, since that's my main stack (at the moment).

Here's a simple diagram for a visual representation:

simple diagram 1

And here are the specific implementations of tokens update TokenRenew() and client authorization UserLogin().

👉 Hey! Full examples of micro-backends (auth and API) can be found here and here. 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 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 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 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

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 (still no need to worry about the DEPRECATED phrase).

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

<!-- ./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>

<!-- ... -->
Enter fullscreen mode Exit fullscreen mode

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);
  2. For smoother transitions, added transition element with fade effect (read more about it here);
  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

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 config (here).

<!-- ./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>
Enter fullscreen mode Exit fullscreen mode

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).
  2. For more convenient work with the Vuex store, I applied 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 (with additional header settings). This helps simplify code separation for a particular business logic. In this case, for token renew requests (see example here).
  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

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.

// ...

// 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️⃣
  }
}

// ...
Enter fullscreen mode Exit fullscreen mode

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

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

// ...

// 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️⃣

// ...
Enter fullscreen mode Exit fullscreen mode

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

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

Photos and videos by

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 support me by donating at LiberaPay. Each donation will be used to write new articles and develop non-profit open-source projects for the community.

Support author at LiberaPay

Discussion (5)

koddr profile image
Vic Shóstak Author

Oh, okay. No problem!

Collapse
koddr profile image
Vic Shóstak Author • Edited on

Hi,

That's not really a relevant comment to this article topic... especially since there's a 404 error on the link! 🤔