N
Nuxt6mo 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"));
}
});
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,
});
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,
});
3 Replies
Dawid
DawidOP6mo ago
I used useCookie() in refreshToken function and it's working good. But is it a good way to do auth?
Chichi / WildSuricate
I have exactly the same question, did you manage the refresh token on SSR?
Goku ごくう
Goku ごくう5mo ago
I am unable to find the resources, but it seems that Pinia cannot be accessed during server-side rendering (SSR), so you will need to create a plugin. I am currently using auth.server.ts under plugins. You can use functions such as useRequestEvent(), appendResponseHeader, and parseCookies from "h3". You can utilize cookies with these functions. Instead of Pinia, I am using useState because it is the safest way for Nuxt payloads, and it works on both the server and client sides. For HttpOnly cookies, here is an example:
const res = await $fetch.raw<Void>(ENDPOINTS.TokenRefresh, {
method: "POST",
baseURL: apiPath,
headers: {
Cookie: authCookieString,
},
});

const setCookieString = res.headers.get("set-cookie") || "";

/**
* `["access_token=...; HttpOnly; ...", "refresh_token=...; HttpOnly; ..."]`
*/
const setCookieArray = splitSetCookieString(setCookieString);

setCookieArray.forEach(cookie => {
appendResponseHeader(event, "set-cookie", cookie);
});
const res = await $fetch.raw<Void>(ENDPOINTS.TokenRefresh, {
method: "POST",
baseURL: apiPath,
headers: {
Cookie: authCookieString,
},
});

const setCookieString = res.headers.get("set-cookie") || "";

/**
* `["access_token=...; HttpOnly; ...", "refresh_token=...; HttpOnly; ..."]`
*/
const setCookieArray = splitSetCookieString(setCookieString);

setCookieArray.forEach(cookie => {
appendResponseHeader(event, "set-cookie", cookie);
});
Auth cookie string is gathered from parseCookies.
Want results from more Discord servers?
Add your server