NextJS refresh token cookie not syncing with browser

3 weeks ago 24
ARTICLE AD BOX

I've been having this hiccup for quite a while. I'm using NextJS 16 to build a fullstack application with separate NestJS backend. The problem is when I perform a refresh logic, the httpOnly cookies that I receive back (containing new access token and refresh token) are not updated to client's territory (the browser), which I understand why that seems to be the problem because of different lifecycle. I'm trying not to do any refresh logic on proxy.ts (previous middleware.ts) and trying to do the refresh logic when Server component already renders (using Tanstack React Query prefetch). The problem still persists where there's not a good single way to sync the new httpOnly cookies I get so browser can update theirs. Here are the implementations. Hoping to get some enlightenment!

Note: I'm using "axios-auth-refresh" to handle concurrent refresh attempts.

// Function to prefetch const prefetchMe = async () => { const queryClient = getQueryClient(); void queryClient.prefetchQuery({ queryKey: queryAuthKeys.me(), queryFn: getMeServer, staleTime: Infinity, }); }; // The queryFn const getMeServer = async ( context: QueryFunctionContext, ): Promise<MeResponse> => { const { signal } = context; const { endpoint, method } = ENDPOINTS.IDENTITY_RELATED.protected.me; const api = await getServerAuthApi(); const res = await api.request<MeResponse>({ url: endpoint, method, signal, }); return res.data; }; // The axios instance used for server calls const getServerAuthApi = cache(async () => { const cookieStore = await cookies(); const apiInstance = axios.create({ baseURL, }); // Create request interceptor apiInstance.interceptors.request.use((config) => { // Check if the Refresh Interceptor has already injected a new cookie // into the defaults, if so, use it and BAIL OUT const memoryCookie = apiInstance.defaults.headers.Cookie; if (memoryCookie) { config.headers.Cookie = memoryCookie; return config; } // Fallback to initial setup // Get both tokens const accessToken = cookieStore.get(ACCESS_TOKEN_COOKIE_NAMESPACE)?.value; const refreshToken = cookieStore.get(REFRESH_TOKEN_COOKIE_NAMESPACE)?.value; // Build the Cookie header string const cookieHeader = [ accessToken ? `${ACCESS_TOKEN_COOKIE_NAMESPACE}=${accessToken}` : "", refreshToken ? `${REFRESH_TOKEN_COOKIE_NAMESPACE}=${refreshToken}` : "", ] .filter(Boolean) .join("; "); if (cookieHeader) { config.headers.Cookie = cookieHeader; } return config; }); createAuthRefreshInterceptor( apiInstance, async (failedRequest) => { const baseAppUrl = process.env.NEXT_PUBLIC_APP_URL; // Calling my own api so that setting cookie is possible const internalRes = await axios.request({ baseURL: baseAppUrl, url: "/api/auth/refresh", method: "POST", headers: { Cookie: cookieStore.toString() }, }); const setCookieHeader = internalRes.headers["set-cookie"]; if (setCookieHeader) { const parsedCookies = parse(setCookieHeader); const newCookieString = parsedCookies .map((c) => `${c.name}=${c.value}`) .join("; "); // This ensures headers are fresh for failed request (the first one) if (failedRequest.headers) { failedRequest.headers.Cookie = newCookieString; } // This ensures subsequent concurrect request(s) get a rresh cookie header apiInstance.defaults.headers.Cookie = newCookieString; } return Promise.resolve(); }, { statusCodes: [401], pauseInstanceWhileRefreshing: true, }, ); return apiInstance; }); // /api/auth/refresh route for calling my backend refresh endpoint (needed so that cookies can be updated) async function POST() { const cookieStore = await cookies(); const refreshToken = cookieStore.get(REFRESH_TOKEN_COOKIE_NAMESPACE)?.value; if (!refreshToken) throw new Error("Aborting refresh, no refresh token available."); try { const { endpoint, method } = ENDPOINTS.IDENTITY_RELATED.public.refresh; const baseURL = process.env.NEXT_PUBLIC_API_URL; const refreshRes = await axios.request({ baseURL, url: endpoint, method, headers: { Cookie: `${REFRESH_TOKEN_COOKIE_NAMESPACE}=${refreshToken};` }, }); const setCookies = refreshRes.headers["set-cookie"]; if (setCookies) { const parsedCookies = parse(setCookies); // Set new cookies parsedCookies.forEach((c) => { cookieStore.set({ name: c.name, value: c.value, path: c.path || "/", maxAge: c.maxAge, httpOnly: c.httpOnly ?? true, secure: c.secure || process.env.NODE_ENV === "production", sameSite: c.sameSite as ResponseCookie["sameSite"], }); }); } return NextResponse.json({ success: true }, { status: 200 }); } catch (error) { console.error("Refresh Route Error:", error); return NextResponse.json({ success: false }, { status: 401 }); } }
Read Entire Article