I have an Express.js backend running on http://localhost:8000 and a Next.js frontend (App Router) running on http://localhost:3000.
What Works: • When making a request from a Next.js client component (browser) to the Express backend, cookies are set successfully. • When checking the response headers in a server component (SSR) or API route, I can see the Set-Cookie header coming from Express.
What Works: • When making a request from a Next.js client component (browser) to the Express backend, cookies are set successfully. • When checking the response headers in a server component (SSR) or API route, I can see the Set-Cookie header coming from Express.
What I Tried: • Setting credentials: "include" in the fetch request. • Making a Next.js API route as a proxy and forwarding the Set-Cookie header. • Using different SameSite policies (Lax, Strict, None with Secure). • Ensuring CORS is properly configured on Express (Access-Control-Allow-Credentials: true). • Manually setting a cookie in the Next.js response (nextResponse.cookies.set("key", "value")).
This is my SSR function on NextJS to fetch request on ExpressJS API. To get info if user is authenticated or not I am sending access token. If access token is expired and refresh token is valid then I am refreshing the access token but new access token is not adding to browser cookies.
"use server";
import { User } from "@/components/providers/user.provider";
import { cookies } from "next/headers";
import { fetcher } from "./fetcher";
interface invalidSession {
ok: false, message: string, code: "unauthorized" | "invalid-or-expired-token"
}
interface successSession {
ok: true, message: string, data: User;
}
export async function getSession() {
const accessToken = (await cookies()).get("accessToken")?.value;
const refreshToken = (await cookies()).get("refreshToken")?.value;
const res = await fetcher<invalidSession | successSession>("/user/info", {
headers: {
Cookie: `accessToken=${accessToken}`
}
});
if (!res.ok) {
if (res.code === "invalid-or-expired-token") {
console.log("Invalid or expired access token");
console.log("refreshing token");
const refreshTokenRes = await fetcher<invalidSession | successSession>("/user/refresh-token", {
method: "POST",
headers: {
Cookie: `refreshToken=${refreshToken}`
}
});
if (!refreshTokenRes.ok) {
console.log("refresh token is invalid or expired!");
return null;
} else {
console.log("refresh token is valid & generated new access token!");
return refreshTokenRes.data
}
}
console.log("No access Token is provided!");
return null;
}
console.log("access token is valid!");
return res.data;
}
this is my expressjs refreshToken function where i setting new accessToken but it not working as i say above. it only works when request comes from nextjs client component but not when request comes from nextjs ssr
public async refreshToken(req: Request, res: Response) {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) {
res.clearCookie("accessToken")
res.clearCookie("refreshToken")
return ResponseHandler.unauthorized(res, "unauthorized");
};
try {
const decoded = jwt.verify(refreshToken, authConfig.auth_refresh_secret) as { id: number };
const user = await prisma.user.findUnique({ where: { id: decoded.id }, include: { user_verifications: true } });
if (!user) {
res.clearCookie("accessToken")
res.clearCookie("refreshToken")
return ResponseHandler.unauthorized(res, "unauthorized")
};
const verifications = user.user_verifications as user_verifications;
const newAccessToken = jwt.sign({ id: user.id }, authConfig.auth_secret, {
expiresIn: `${authConfig.auth_secret_expires_in as any}m`
})
res.cookie("accessToken", "asd", {
httpOnly: true,
secure: process.env.NODE_ENV === "production" ? true : false,
sameSite: "strict",
});
return ResponseHandler.success(res, {
accessToken: newAccessToken,
user: {
firstname: user.firstname,
lastname: user.lastname,
email: user.email,
email_verified: verifications.email_verified,
email_verified_at: verifications.email_verified_at,
phone: user.phone,
phone_verified: verifications.phone_verified,
phone_verified_at: verifications.phone_verified_at,
}
})
} catch (error) {
res.clearCookie("accessToken")
res.clearCookie("refreshToken")
return ResponseHandler.unauthorized(res, "unauthorized", "Invalid refresh token");
}
}