I am using Nuxt 3 with a Node.js remote server, and I’m having trouble setting up an authentication middleware on my Nuxt frontend. Here is my current code in the /middleware/auth.global.js file:
// auth.global.js
import { defineNuxtRouteMiddleware, navigateTo } from '#app';
export default defineNuxtRouteMiddleware(async () => {
const apiBase = useRuntimeConfig().public.baseUrl;
const checkLogin = async () => {
try {
const response = await $fetch(`${apiBase}/is_loggedin`, {
method: 'GET',
credentials: 'include',
});
return response;
} catch (error) {
console.error("Error while checking login status:", error);
return { isLoggedin: false };
}
};
const response = await checkLogin();
if (!response || !response.isLoggedin) {
return navigateTo('/login');
}
});
This middleware works fine when navigating between routes on the page, but it doesn't behave correctly when the user refreshes the page. Upon refreshing, the user is always redirected to the /login page, even if they are logged in.
On the backend, when a user logs in, the server sets a JWT authToken cookie, which can be accessed via the server's get/set APIs. Here is how the cookie is set on the server:
res.cookie("authToken", token, {
httpOnly: true,
secure: true,
sameSite: "None",
maxAge: 24 * 60 * 60 * 1000, // 1 day
});
If possible, I would like to read the data from this JWT cookie (as it contains user roles and other information) and then create additional middleware for my app.
My Questions:
I am especially interested in secure solutions to prevent attackers from tampering with the authToken cookie or abusing it in any way.
I'm fairly new to Nuxt and this kind of setup, so any guidance or suggestions would be greatly appreciated.
Thank you in advance!
If you want to build an entirely custom authentication solution—meaning without using external packages or Nuxt libraries—you'll need to follow some specific rules, which I’ll outline below.
Alternatively, you can use Nuxt libraries such as nuxt-utils, which provide built-in support for sessions and cookie encryption by default. That way, you don’t have to handle everything manually, although doing it yourself can be an excellent learning experience.
I already did that, so below are the results of my research.
To ensure everything works correctly even when the user refreshes the page, it’s important to use plugins together with stores. Plugins are pieces of code that run before the app is rendered, both on the client side and the server side.
I usually follow the strategy below, it is the same pattern recommended by vue-mastery courses. I’m assuming you’re already storing the cookie with the encrypted JWT, so the getTokenFromCookie function should then read the cookie and decrypt it.
1. Store that fetches the user data:
// stores/user.ts
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
user: "" as string,
}),
actions: {
async fetchItems() {
if (!this.user) {
const { data } = await useFetch('/api/user')
this.user = data.value?.user ?? ""
}
},
async logout() {
this.user = ""
},
},
})
2. Endpoint that retrieves the token, fetches the user, and is used in the stores:
// server/api/user.ts
export default defineEventHandler(async (event) => {
const vars = useRuntimeConfig();
const serverUtils = new ServerUtils(event);
const token = serverUtils.getTokenFromCookie();
if (token) {
const res = await fetch(`${vars.public.API_BASE_PATH}/user/me`, {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
}
});
if (res.ok) {
const user = await res.text();
return { user };
} else {
return createError({
statusCode: 403,
message: "Not Authorized",
});
}
} else {
return createError({
statusCode: 403,
message: "Not Authorized",
});
}
});
3. Server-side middleware that reads the cookie and loads it into a context:
// server/middleware/01.middleware.ts
export default defineEventHandler(async (event) => {
const vars = useRuntimeConfig()
const serverUtils = new ServerUtils(event)
const token = serverUtils.getTokenFromCookie();
if (token) {
try {
const res = await fetch(`${vars.public.API_BASE_PATH}/user/me`, {
headers: {
Authorization: `Bearer ${token}`
}
})
if (res.ok) {
const user = await res.text();
event.context.user = user
} else {
serverUtils.clearTokenCookie();
}
} catch (err) {
serverUtils.clearTokenCookie();
}
}
});
4. Plugin that reads these data and loads the variables before the app starts:
// plugins/00.auth.ts
import { useUserStore } from "~/stores/auth";
export default defineNuxtPlugin((nuxtApp) => {
const userStore = useUserStore();
if (!userStore.user && nuxtApp.ssrContext?.event.context.user) {
userStore.user = nuxtApp.ssrContext.event.context.user;
}
if (!userStore.user) {
userStore.fetchItems();
}
});
If you want to handle authorization on the server side, you can follow the same structure and deny or redirect the page load in a server-side middleware. Otherwise, you can filter it using client-side middleware. Both approaches will work effectively and securely.