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:
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
andpassword
), the backend sends a JSON response with simple session information (JWTaccess_token
andexpire
timestamp) and a special HttpOnly Cookie with an encryptedrefresh_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 theHttpOnly
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;
- If the
⚡️ 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.
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>
<!-- ... -->
The most common root Vue template for all the views in our application. And what happens in this code snippet:
- Create a new root element
router-view
(read more about it here); - For smoother transitions, added
transition
element withfade
effect (read more about it here); - 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;
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>
Perfect! Now let's review the important points:
- 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). - For more convenient work with the Vuex store, I applied constants for mutation types.
- 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). - 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).
- 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.
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️⃣
}
}
// ...
Not complicated, is it? Let's go into more detail:
- Save the response data (a new JWT and user info) to the Vuex store.
- 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. - Get current route name from
vue-router
. - 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. - Show any other errors.
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️⃣
// ...
And that's what we're doing here:
- Subtract 1 minute from JWT
expire
field. - If expire time is less or equal than now, and
localStorage
has_myapp
item, then send request to renew token. - Set 1 minute interval to make the periodic request.
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! 😎
Photos and videos by
- Vic Shóstak https://shostak.dev
P.S.
If you want more articles (like this) on this blog, then post a comment below and subscribe to me. Thanks! 😻
❗️ You can support me on Boosty, both on a permanent and on a one-time basis. All proceeds from this way will go to support my OSS projects and will energize me to create new products and articles for the community.
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 main projects that need your help (and stars) 👇
- 🔥 gowebly: A next-generation CLI tool that makes it easy to create amazing web applications with Go on the backend, using htmx, hyperscript or Alpine.js and the most popular CSS frameworks on the frontend.
- ✨ create-go-app: Create a new production-ready project with Go backend, frontend and deploy automation by running one CLI command.
Top comments (0)