Procházet zdrojové kódy

refactor: :recycle: the refresh token now is a httpOnly token to be more secure, with that now the access token is only saved on the user store an the route and permissions flows changed a bit

Denis před 9 měsíci
rodič
revize
c4c9cfb410

+ 0 - 1
quasar.config.js

@@ -18,7 +18,6 @@ export default configure((ctx) => {
       "axios",
       "i18n",
       "defaultPropsComponents",
-      "setPermissions",
       // "socket.io",
     ],
 

+ 9 - 25
src/boot/axios.js

@@ -2,17 +2,16 @@ import { boot } from "quasar/wrappers";
 import { Cookies, Notify } from "quasar";
 import axios from "axios";
 import { useRouter } from "vue-router";
-import { useAuth } from "src/composables/useAuth";
+import { userStore } from "src/stores/user";
 
 const api = axios.create({
   baseURL: process.env.API_URL + "/api",
   withCredentials: true,
-  withXSRFToken: true,
 });
 
 api.interceptors.request.use(
   async (config) => {
-    const accessToken = Cookies.get("access_token");
+    const accessToken = userStore().accessToken;
     const savedLanguage = Cookies.get("locale");
     const language = savedLanguage || window.navigator.language;
     config.headers["Accept-Language"] = language;
@@ -41,13 +40,9 @@ const processQueue = (error, token = null) => {
   failedQueue = [];
 };
 
-const errorInterceptor = async (
-  error,
-  router,
-  setAuthTokens,
-  eraseAuthTokens,
-) => {
+const errorInterceptor = async (error, router) => {
   const originalRequest = error.config;
+  const user_store = userStore();
 
   if (error.response?.status === 422) {
     return Promise.reject(error);
@@ -75,30 +70,20 @@ const errorInterceptor = async (
   if (isRefreshing) {
     return new Promise((resolve, reject) => {
       failedQueue.push({ resolve, reject, config: originalRequest });
-    })
-      .then((res) => res)
-      .catch((err) => Promise.reject(err));
+    });
   }
 
   isRefreshing = true;
   try {
-    const refreshToken = Cookies.get("refresh_token");
-    if (!refreshToken) {
-      router.push("/login");
-      return Promise.reject(new Error("No refresh token available."));
-    }
-
-    const response = await api.post("/refresh", {
-      refresh_token: refreshToken,
-    });
+    const response = await api.post("/refresh");
     const newAccessToken = response.data.payload.access_token;
-    setAuthTokens(response.data.payload);
+    user_store.accessToken = newAccessToken;
     originalRequest.headers["Authorization"] = `Bearer ${newAccessToken}`;
     processQueue(null, newAccessToken);
     return api(originalRequest);
   } catch (err) {
     processQueue(err, null);
-    eraseAuthTokens();
+    user_store.resetUser();
     router.push("/login");
     return Promise.reject(err);
   } finally {
@@ -118,11 +103,10 @@ const successInterceptor = (response) => {
 
 export default boot(({ app }) => {
   const router = useRouter();
-  const { setAuthTokens, eraseAuthTokens } = useAuth();
 
   api.interceptors.response.use(
     (response) => successInterceptor(response),
-    (error) => errorInterceptor(error, router, setAuthTokens, eraseAuthTokens),
+    (error) => errorInterceptor(error, router),
   );
 
   // for use inside Vue files (Options API) through this.$axios and this.$api

+ 0 - 6
src/boot/setPermissions.js

@@ -1,6 +0,0 @@
-import { boot } from "quasar/wrappers";
-import { permissionStore } from "src/stores/permission";
-
-export default boot(async () => {
-  await permissionStore().fetchScopes();
-});

+ 11 - 36
src/composables/useAuth.js

@@ -1,30 +1,12 @@
 import api from "src/api";
-import { Cookies } from "quasar";
 import { permissionStore } from "src/stores/permission";
 import { userStore } from "src/stores/user";
 
 export const useAuth = () => {
-  const setAuthTokens = async (tokens) => {
-    const { access_token, refresh_token } = tokens;
-    const accessTokenExpiresIn = new Date(
-      new Date().getTime() + tokens.access_token_expires_in * 1000,
-    );
-    const refreshTokenExpiresIn = new Date(
-      new Date().getTime() + tokens.refresh_token_expires_in * 1000,
-    );
-    Cookies.set("access_token", access_token, {
-      expires: accessTokenExpiresIn,
-    });
-    Cookies.set("refresh_token", refresh_token, {
-      expires: refreshTokenExpiresIn,
-    });
-    userStore().user = tokens.user;
-    await permissionStore().fetchScopes();
-  };
-
-  const eraseAuthTokens = async () => {
-    Cookies.remove("access_token");
-    Cookies.remove("refresh_token");
+  const setAuthDataFromPayload = async (tokens) => {
+    const { access_token, user } = tokens;
+    userStore().user = user;
+    userStore().accessToken = access_token;
     await permissionStore().fetchScopes();
   };
 
@@ -36,8 +18,7 @@ export const useAuth = () => {
       });
 
       if (response.status === 200) {
-        const payload = response.data.payload;
-        await setAuthTokens(payload);
+        await setAuthDataFromPayload(response.data.payload);
       }
       return response;
     } catch (error) {
@@ -49,24 +30,20 @@ export const useAuth = () => {
     try {
       const response = await api.post("/logout");
       if (response.status === 200) {
-        await eraseAuthTokens();
+        userStore().resetUser();
       }
     } catch (error) {
       console.error(error);
     }
   };
 
-  const refreshToken = async () => {
+  const refresh = async () => {
     try {
-      const refresh_token = Cookies.get("refresh_token");
-      const response = await api.post("/refresh", {
-        refresh_token: refresh_token,
-      });
-
+      const response = await api.post("/refresh");
       if (response.status === 200) {
-        const payload = response.data.payload;
-        await setAuthTokens(payload);
+        await setAuthDataFromPayload(response.data.payload);
       }
+      return response;
     } catch (error) {
       return Promise.reject(error);
     }
@@ -75,8 +52,6 @@ export const useAuth = () => {
   return {
     login,
     logout,
-    refreshToken,
-    setAuthTokens,
-    eraseAuthTokens,
+    refresh,
   };
 };

+ 13 - 7
src/router/index.js

@@ -6,9 +6,11 @@ import {
   createWebHashHistory,
 } from "vue-router";
 import routes from "./routes";
-import { Cookies, Notify } from "quasar";
+import { Notify } from "quasar";
 import { permissionStore } from "src/stores/permission";
 import { useI18n } from "vue-i18n";
+import { userStore } from "src/stores/user";
+import { useAuth } from "src/composables/useAuth";
 /*
  * If not building with SSR mode, you can
  * directly export the Router instantiation;
@@ -34,19 +36,23 @@ export default route(function (/* { store, ssrContext } */) {
     // quasar.conf.js -> build -> publicPath
     history: createHistory(process.env.VUE_ROUTER_BASE),
   });
-
+  let refreshed = false;
   Router.beforeEach(async (to, from, next) => {
-    const { getAccess } = permissionStore();
-    const access_token = Cookies.get("access_token");
-    if (to.meta.requireAuth && !access_token) {
-      return next({ name: "LoginPage" });
+    if (userStore().accessToken == null && !refreshed) {
+      try {
+        await useAuth().refresh();
+      } catch {
+        refreshed = true;
+        return next({ name: "LoginPage" });
+      }
     }
-    if (access_token) {
+    if (userStore().accessToken) {
       if (to.name == "LoginPage") {
         return next({ name: "DashboardPage" });
       }
     }
     if (to.meta.requiredPermission) {
+      const { getAccess } = permissionStore();
       const permission = getAccess(to.meta.requiredPermission, "view");
       if (!permission) {
         Notify.create({

+ 3 - 7
src/stores/permission.js

@@ -2,7 +2,6 @@ import { defineStore } from "pinia";
 import { ref, computed } from "vue";
 import { userStore } from "src/stores/user";
 import { getUserPermissions, getGuestPermissions } from "src/api/permission";
-import { Cookies } from "quasar";
 
 export const permissionStore = defineStore("permission", () => {
   const bitwisePermissionTable = Object.freeze({
@@ -95,13 +94,10 @@ export const permissionStore = defineStore("permission", () => {
 
   const fetchScopes = async () => {
     try {
-      const accessToken = Cookies.get("access_token");
+      const { accessToken } = userStore();
       if (accessToken) {
-        const [userPermissions] = await Promise.allSettled([
-          getUserPermissions(),
-          userStore().fetchUser(),
-        ]);
-        permissions.value = userPermissions.value;
+        const userPermissions = await getUserPermissions();
+        permissions.value = userPermissions;
       } else {
         const response = await getGuestPermissions();
         permissions.value = response;

+ 3 - 0
src/stores/user.js

@@ -4,6 +4,7 @@ import { getUser } from "src/api/user";
 
 export const userStore = defineStore("user", () => {
   const user = ref(null);
+  const accessToken = ref(null);
   const isAdmin = ref(false);
 
   const setUser = (userData) => {
@@ -14,6 +15,7 @@ export const userStore = defineStore("user", () => {
   const resetUser = () => {
     user.value = null;
     isAdmin.value = false;
+    accessToken.value = false;
   };
 
   const fetchUser = async () => {
@@ -27,5 +29,6 @@ export const userStore = defineStore("user", () => {
     setUser,
     resetUser,
     fetchUser,
+    accessToken,
   };
 });