Help request: Tokens cookie refresh global issue in Next.js 14

Intro

I’m new to development and it’s more of a hobby for me. As part of my personal project, I faced a simple task: client-server interaction between a NestJS server and Next.js client. Following the latest requirements, I implemented authorization through a pair of tokens with different lifetime and storing them in the client’s browser cookie. And this is where the adventure began.

Problem

Next.js imposes very strict restrictions on working with cookies, so you can’t just write new tokens received. This must happen in Server Actions or Route Handlers. You can also do it in middleware. The Internet is full of instructions on how to bypass these restrictions using Axios, third-party libraries for working with cookies, wrapping requests to an external server in Route Handlers, etc.
I tried them all, and none of them worked well.

In addition, I strongly dislike the idea of putting something in the system to replace existing tools because I couldn’t figure them out. The Next.js developers designed it with fetch() in mind as a tool for executing queries, deliberately put restrictions on working with cookies, created Server Actions and Route Handlers for purposes unrelated to third-party APIs.

Thus, the question arises: how to implement the process of updating tokens with storage in browser cookies using built-in Next.js tools?

Current state

Thanks to the person who created the FAQ for the Next.js Discord Server, he gave me an idea and I was able to get some results. Currently implemented:

  • Retrieving tokens from the server and saving to cookies
  • Controlling through middleware the state of the cookie and updating it if necessary.

And everything seems to work, but there is a problem: the corner case when a page was loaded with a valid token, and then (say, right after the load is complete) the token expires.

On the next request (navigating to another page, for example), the app behaves unpredictably, using the cache almost randomly.

To demonstrate the problem, I created two public repositories and placed them on free hosting:

Token lifetimes are intentionally set very short: 30 seconds and 3 minutes. You can check for yourself how weird it looks when AccessToken has already expired, middleware has already updated both tokens, and the application is still running with cache.

Something like revalidatePath('/', 'layout') would be very useful here, but this is impossible in middleware.

Question for the community

Is there any way to update tokens in cookies without losing the consistency of the global state of the application, and without using third-party libraries or dirty hacks?

Hi, @zakharsk!

The core issue here is maintaining consistency between the authentication state (stored in cookies) and the application’s cache, especially when tokens expire. Next.js’s caching mechanism is powerful but can lead to inconsistencies in authentication states if not handled carefully.

You could:

  1. Create a Server Action for token refresh. This action will update the tokens in cookies and use revalidatePath('/') to revalidate the entire application.
  2. Implement a client-side component that periodically checks for token expiration. If a token is about to expire, it triggers the refresh action and uses router.refresh() to ensure all components re-render with the new state.
  3. Use Route Handlers as proxies for your API calls. These handlers will always use the latest tokens from cookies, ensuring that API requests are made with up-to-date credentials.
  4. Implement careful cache management in your pages by using { cache: 'no-store' } in data fetching functions. This ensures that fresh data is always fetched, preventing issues with stale authentication states.
Here's a basic example of how the `TokenRefresher` component might look:
'use client'

import { useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { refreshTokens } from '../actions/auth'

export function TokenRefresher() {
  const router = useRouter()

  useEffect(() => {
    const checkTokenExpiration = async () => {
      const isExpiringSoon = /* Your logic to check token expiration */

      if (isExpiringSoon) {
        const success = await refreshTokens()
        if (success) {
          router.refresh()
        } else {
          router.push('/login')
        }
      }
    }

    const intervalId = setInterval(checkTokenExpiration, 10000) // Check every 10 seconds

    return () => clearInterval(intervalId)
  }, [router])

  return null
}

This approach addresses the corner case you mentioned where a token expires shortly after page load. The periodic checks by the TokenRefresher component will catch such cases and refresh the tokens as needed, maintaining application consistency.

Let us know how you get on!

Helpful resources
1 Like

Hey, @pawlean.

Thanks for the detailed reply and for the idea of a client component to refresh tokens.

I was able to implement the proposed solution and at a glance it looks like everything works. Timer requests to the server unnecessarily is a questionable solution, but that’s a matter of fine tuning.

The only thing that remains a mystery to me is item 3 of your list.

In my implementation I use a wrapper for fetch, which reads cookies at each call and adds the current token to the request. What did you envision proxying requests to the API via Route Handlers for?

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.