Next.js Discord

Discord Forum

What's the best way to store and refresh jwt

Unanswered
Masai Lion posted this in #help-forum
Open in Discord
Masai LionOP
My team is using api service to fetch data, we pass jwt token to authorize the request. Previously we used to store the jwt in a cookie or localstorage and whenever api would return 401 we would refresh the token and update cookie/localstorage.

This causes me trouble with nextjs migration, we are using wrapper around fetch which would refresh the token if api returns 401 but i cannot seem to be able to update the cookie im storing the jwt in. I don't know if the fetch will be called in client or server component so i thought of using server actions to update the cookie but then i realized i cannot set cookies in server actions.

Does anyone have alternative approach? next-auth seemed like overkill since i dont have any api endpoints in the nextjs app neither do i use ouath or have sign in page (we are using web3 to sign in inside modal) so the next-auth seemed out of place

21 Replies

I cannot set cookies in server actions
No, it's allowed to set cookies in server actions. Make sure you have returned a response in the action

const cookies = cookies();
cookies.set(...);
return NextResponse.json({ ... });

Will work in server actions, unless you're trying to invoke it on the server side (inside of a server component), [learn more about cookies](https://nextjs.org/docs/app/api-reference/functions/cookies#cookiessetname-value-options)

And as @European sprat mentioned, you can also use a middleware instead, but cookies are not available in middlewares, you have to handle it manually
European sprat
Use middleware to check and update cookie
Masai LionOP
@European sprat would my middleware intercept the request if its not to my domain but third party api
Masai LionOP
nvm i see that its only supposed to intercept request to my app not external requests. So im still not sure how to resolve this
Your middleware can only intercept requests sent to your next.js application, including a page, or an api endpoint. It’s not what you’re looking for if you’re not intended to implement authentication.
In your case, you want to refresh the token when you received a 401 response. However, what if the fetch is happened on server?
Generally, we will use an api key on the server. If you want to fetch the data from client side, just use your server as a proxy to interact with the service.
@Masai Lion <@135324139648057344> would my middleware intercept the request if its not to my domain but third party api
European sprat
is the request going to your nextjs server or not? whichever server receives the request will need to do the check and regen for jwt
Masai LionOP
its our api service not frontend nextjs server, im trying to write what Fuma mentioned with the proxy, have generic /api route in the nextjs app that handles all methods GET/POST etc. it reads the actual request url and data from request body and forwards it to our actual api service if it returns 401 im asking to renew it there
Masai LionOP
actually maybe i can just make request to the /api to update the cookie rather than forwarding the entire request there? so i handle everything as i did before however when i renew my token i do request to /api or /cookie which would make more sense to set the new cookie?
It's also a common way to update the cookies
as above, you can update cookies in a route handler or server action, so it is absolutely possible
Masai LionOP
thanks a lot im almost there, the only confusing part for me right now is that in my api i do:

export async function PUT(
req: Request,
) {
cookies().set('jwt', req.headers.get('Authorization') ?? ''");
return NextResponse.json({})
}

then after refreshing i want to retry the request, i always get the cookie like this:
const jwtCookie = cookies().get('jwt')?.value
return jwtCookie

but that returns me the previous value for some reason before refreshing i have double checked and the route receives the updated jwt so i don't understand why the get still has old value after set
the UI won't be updated instantly, you have to run router.refresh() to revalidate router cache (aka client-side cache)
Masai LionOP
hmm im sorry to bother you about this but i must be getting something wrong about next and how it works.

The testing setup right now looks like this:

page.tsx where i make two calls in parallel to test whether my retrying works, the page.tsx is a server component.

the function to fetch data looks like this:

export const getBotStatus = async (address: string, generate?: boolean, iter?: number): Promise<BotsInfo[]> => {
  const url = `${BASE_URL}/wallet-tracker/users/${address}/bots-status/?generate=${generate}&iter=${iter}`;
  const options = { method: 'GET' };
  try {
    return await fetcher(url, options)
  } catch (error) {
    throw error;
  }
}


then the fetcher that contains retry logic:
const APP_URL = process.env.NEXT_PUBLIC_APP_URL

let isRefreshing = false;

export const fetcher = async <T>(input: RequestInfo, init?: RequestInit): Promise<T> => {
  const jwtToken = await actionGetServerJWT()
  const auth = jwtToken ? { Authorization: `Bearer ${jwtToken}` } : undefined;
  const headers = { ...auth, ...init?.headers };
  const response = await fetch(input, {...init, headers });

  if (response.ok) return await response.json() as Promise<T>;
  if (response.status !== 401) throw await fetchErrorHandler(response);
  
  const retryAfterRefresh = async (): Promise<T> => {
    await waitFor(1000) // Wait a second before checking if we are still refreshing
    if (isRefreshing) return await retryAfterRefresh() // If we are still refreshing, try again
    return await fetcher(input, init) // If we are done refreshing, retry the request which will read the new jwt
  }
  
  /* If another request is already refreshing, retry it after token is refreshed  */
  if (isRefreshing) return await retryAfterRefresh();
  /* Stop other parallel requests from renewing multiple times */
  isRefreshing = true;
  
  const res = await fetch(`${BASE_URL}/auth/renew/`, { method: 'POST', headers })
 if (!res.ok) throw await fetchErrorHandler(response)
  const jwt = await res.json()
  
  /* Update the cookie on the server */
  await fetch(`${APP_URL}/api/cookies`, { method: 'PUT', headers: { 'Authorization': JSON.stringify(jwt) } })
  /* Let other requests know we are done refreshing */
  isRefreshing = false;
  /* Retry the request that was responsible for refreshing */
  return await fetcher(input, {...init, headers: { Authorization: `Bearer ${jwt.token}` }});
};

export const fetchErrorHandler = async (response: Response) => {
  const {error} = await response.json()
  return ({message: error, code: response.status})
}

(Sorry i dont have nitro)

the fetcher and get bot status are simple files that do not use client. The idea is that getBotStatus can be called from server and client components depending on where its needed, maybe this approach is bad and each request should be handled on its own in the component but then i dont see how i can prevent repeating this retry logic
Normally, if we caught the token is expired on the client side, just go back to the sign-in page.
For refreshing the token, we won't check it for individual requests, instead, check and refresh when the client loads the page.
Masai LionOP
thanks, this helped a lot. I solved my issue by having the api set cookie and read it from incoming requests. 🙂
requests on the server will be dealt with differently