Vue 3, Vue Router 4 Navigation Guards and Pinia store
Asked Answered
E

2

5

I'm trying to create an Vue 3 with app with JWT authentication and meet an issue with guarding the router using "isAuth" variable from Pinia store to check the access. Eventually Vue router and app in whole loads faster than the Store, that's why I'm always getting "unauthorized" value from the store, but in fact user is logged in and his data is in store. I'll try to describe all the steps that are made to register and login user.

  1. Registration is made to NodeJS backend and JWT token is created.
  2. On the login screen user enters email and password, if info is valid he will be logged in and JWT will be saved to localstorage and decoded through JWTdecode, decoded token data will be saved to the store in user variable, and isAuth variable set to true.
  3. Pinia store has 2 fields in state: user(initially null), and isAuth(initially false).
  4. In the main App component I'm using async onMounted hook to check the token and keep user logged in by calling the API method, which compares JWT.
  5. In the Vue router i have several routes that must be protected from the unauthorized users, that's why I'm trying to create navigation guards for them by checking the user information from the store. Problem is, router is created after the setting user info and is always getting the initial state of the user and isAuth variables.

Code:

Store

import { defineStore } from 'pinia';

export const useLoggedInUserStore = defineStore({
  id: 'loggedInUser',
  state: () => ({
  isAuth: false,
  user: null
   }),

  getters: {
  getisAuth(state) {
  return state.isAuth;
    },
  getUser(state) {
  return state.user;
   }
  },
 actions: {
  setUser(user) {
  this.user = user;
  },
  setAuth(boolean) {
  this.isAuth = boolean;
   }
}
});

App.vue onMounted

 onMounted(async () => {
    await checkUser()
      .then((data) => {
         isLoading.value = true;
          if (data) {
          setUser(data);
          setAuth(true);
         } else {
         router.push({ name: 'Login' });
          }
       })
       .finally((isLoading.value = false));
       });

Router guard sample

router.beforeEach((to, from, next) => {
   const store = useLoggedInUserStore();
   if (!store.isAuth && to.name !== 'Login') next({ name: 'Login' });
   else next();
});

I feel that problem is with this async checking, but can't figure out how to rewrite it to load store before the app initialization.

I hope that somebody meet this problem too and can help.

Thanks in advance!

Etoile answered 31/3, 2022 at 18:23 Comment(0)
G
13

So I just met this problem and fixed it thanks to this solution

As it says, the router gets instantiated before App.vue is fully mounted so check the token in beforeEach instead, like:

router.beforeEach(async (to, from, next): Promise<void> => {
  const user = useUser();
  await user.get();

  console.log(user) // user is defined

  if (to.meta.requiresAuth && !user.isLoggedIn) next({ name: "home" }); // this will work

By the way instead of having an action setAuth you could just use your getter isAuth checking if user is not null, like:

isAuth: (state) => state.user !== null

Also it's not recommended to store a JWT in the local storage as if you're site is exposed to XSS attacks the token can be stolen. You should at least store it in an HttpOnly cookie (meaning it's not accessible from JavaScript), it's super easy to do with Express.

Gradualism answered 4/4, 2022 at 13:52 Comment(3)
Thank You! About the localStorage. Yes, i konw, this is only demo app, but i'll change my token logic to store it in more secure place.Etoile
I don't think hiding the token is worth any effort... the token is sent anyway with every request so it can still be easily retrieved and even manipulated during the manual request ... it's very important to set a reasonable small expiry and use refresh tokens ...Rosenberg
Putting the JWT in an HttpOnly cookie protects it from XSS attacks, as Bryan points out, and that's worthwhile. Even though, sure, it's still visible in the network traffic, the idea with that detail isn't to protect against attacks by the app user. Agreed on the short expiry time!Warmedover
M
4

First of all, I want to say that using App.vue and the lifecycle hooks is a bad implementation, try not to use it like that.

Create authGuard.ts:

import { useUserStore } from '@/stores/user';
import type { NavigationGuard } from 'vue-router';

export const authGuard: NavigationGuard = async (to, from, next) => {
  const userStore = useUserStore();

  // Fetch user data from the server or local storage
  await userStore.fetchUser();

  // Check if the user is authenticated
  if (userStore.isAuthenticated) {
    next();
  } else {
    // You can use try/catch to get an id token and set it to your request header
    // ex: try { ... next() } catch { ... next({ name: '/login') }
    next('/login');
  }
};

Add the router guard to your router configuration:

import { createRouter, createWebHistory } from 'vue-router';
import { authGuard } from '@/guards/authGuard';

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      component: Home,
      meta: { requiresAuth: true },
    },
    {
      path: '/login',
      component: Login,
    },
  ],
});

router.beforeEach((to, from, next) => {
  if (to.matched.some(record => record.meta.requiresAuth)) {
    authGuard(to, from, next);
  } else {
    next();
  }
});

export default router;

You can define your custom isAuthenticated checker in the store file.

Hope it helps! It's a basic implementation and should be refactored according to your needs.

Martensite answered 18/3, 2023 at 9:4 Comment(1)
Won't this cause the store to re-fetch the from the server for every route change though? That might be undesired behaviour in some cases.Brahmin

© 2022 - 2024 — McMap. All rights reserved.