NuxtN
Nuxt2y ago
Dawid

Issue with JWT auth in Nuxt 3 SSR

Hello everyone!

I'm experiencing an issue with JWT authentication in my Nuxt 3 SSR application. The problem occurs when trying to access protected routes directly (by entering the URL) rather than navigating through the app.

Current setup:
  • Using middleware to check if a user is logged in and redirect to login if necessary
  • Access token is stored in an auth store (Pinia)
  • Refresh token is stored in an HttpOnly cookie
  • Auth middleware checks token validity and refreshes if needed (sends a request to refresh token endpoint with refresh token cookie)
The issue:
When navigating within the app using Nuxt Link, everything works as expected. If the access token is invalid, a request is sent to refresh it, and I can access protected routes normally.

However, when I try to access a protected route directly by entering its URL, the middleware seems to run on the server-side without access to the HttpOnly cookie containing the refresh token. As a result, the refresh request fails, and I'm unable to access the protected route.

Could you please help me what's the best way to fix it?

middleware/auth.ts:
export default defineNuxtRouteMiddleware(async (to, from) => {
  const authStore = useAuthStore();
  const localePath = useLocalePath();
  console.log('started auth middleware')

  if (!authStore.token) {
    return navigateTo(localePath("/login"));
  }

  const token = authStore.token;
  try {
    const decoded: any = jwtDecode(token);
    const currentTime = Date.now() / 1000;

    if (decoded.exp < currentTime) {
      const response = await authStore.refreshToken();
      if (response.status != "ok") {
        throw new Error("Token expired");
      }
      console.log(response, "response refresh token middleware auth");
    }
  } catch (err) {
    console.log(err)
    authStore.logout();
    return navigateTo(localePath("/login"));
  }
});


STORE/auth.ts
import { defineStore } from "pinia";
const baseUrl = process.env.API_BASE_URL;
console.log(baseUrl);

export const useAuthStore = defineStore({
  id: "auth",
  state: () => ({
    user: null,
    token: null,
  }),
  actions: {
    async login(loginForm) {
      await useAPI(`/auth/login`, {
        method: "POST",
        body: loginForm,
        credentials: "include",
      })
        .then((response) => {
          console.log(response.data.value, "tutaj");
          this.user = response.data.value;
          this.token = this.user.jwt;
          console.log(this.user, this.token, "after state");
        })
        .catch((error) => {
          throw error;
        });
    },
    async register(registerData) {
      await $fetch(`${baseUrl}/register`, {
        method: "POST",
        body: registerData,
        credentials: "include",
      })
        .then((response) => {
          this.user = response;
          this.token = this.user.jwt_token;
        })
        .catch((error) => {
          throw error;
        });
    },
    async refreshToken() {
      try {
        const response = await useAPI(`/api/auth/refresh-token`, {
          method: "POST",
          body: {},
          credentials: "include",
        });
        console.log(response.data);
        this.token = response.data.value.jwt;
        this.user = response.data.value.user;
        return { status: "ok" };
      } catch (e) {
        console.log(e);
        this.logout();
      }
    },
    logout() {
      this.user = null;
      this.token = null;
    },
    setToken(token) {
      this.token = token;
    },
    setUser(user) {
      this.user = user;
    },
  },
  persist: true,
});
Was this page helpful?