Next.js Discord

Discord Forum

Authentication / Refresh token process

Answered
Korat posted this in #help-forum
Open in Discord
KoratOP
Hello guys, I've been trying soo long to make a working authentication & refresh token process for my nextjs app router / dotnet rest api applications.

I've started implementing my own auth since next-auth doesn't work well in my case.

I have a fetch wrapper that takes the access_token and forwards it to my dotnet api as bearer token to access my private data.

I have created server actions to set an httponly cookie on login / remove the cookie on logout but the issue arises with refresh token.

What i'm thinking is to handle this inside the fetch wrapper shown in the picture, whenever my rest api returns 401 unauthorized I would reset my cookie with the newest data that comes in the response but the thing is that It won't let me access cookies() from this wrapper even tho its marked with 'use server' saying that "Error: Cookies can only be modified in a Server Action or Route Handler."

I don't know where else to handle the situation,

Thanks a lot for your time.
Answered by Ray
its better to handle it in middleware i guess and try to make the fetch wrapper later :lolsob:
currently, we can't set the cookie from the page due to http streaming
View full answer

84 Replies

Answer
KoratOP
I tried to handle it in middleware but it wont let me change the cookie too or even delete and set a new one
req.cookies.get() req.cookies.set() req.cookies.delete() should work
KoratOP
req.cookies.set but it showed the same error
this should work
const res = NextResponse.next()
res.cookies.set('cookie', 'value')
return res
KoratOP
didn't try this one lemme check
This seemed to work, im gonna continue improving the logic hopefully i wont run into an issue.

I don't know if returning res at that line is good idea since i need to return i18nrouting but its probably okay since i dont know how middleware runs in background
Thanks for your help, i guess the only solution is handling in middleware and not in fetch wrapper
you could set the cookies to the response return from handleI18nRouting()
KoratOP
hmm good idea
lemme change the code
seems to work good now thanks a lot once more,

can i leave this thread open just in case I run into any issue?
sure
KoratOP
Thank you Ray, I think its working better now, daym can't believe im dealing with authentication and refresh for months now haha
KoratOP
Thing is that the fetch wrapper is being executed at every request and therefor is being triggered before middleware reaches the condition if the access_token has expired which in most of the cases throws an error that the token has expired.

I wish there was a way to handle it inside the fetch wrapper
KoratOP
Hmm, yeah you are right, the middleware is running first,

I think the condition to check if the token has expired is uncorrect.

if (dayjs().isAfter(dayjs(session.refreshTokenExpireTime)))

Ill check it out in more depth
KoratOP
Weird thing happening is sometimes even tho i replace the cookie with the new token the fetch wrapper uses the old one which triggers an api response error with 401 saying that the token is invalid (probably the race condition issue as we have seen happening with next-auth too)
@Korat Weird thing happening is sometimes even tho i replace the cookie with the new token the fetch wrapper uses the old one which triggers an api response error with 401 saying that the token is invalid (probably the race condition issue as we have seen happening with next-auth too)
I think you need to pass the new token to header too when refresh happen like this
const res = handleI18nRouting(request)
if (needRefresh) {
  res.headers.set("Authorization": `Bearer ${newToken}`)
}
return res

and in fetchWrapper
...(authCookie && { ... }),
...init.headers,
because the new cookie should only send in next request
KoratOP
Interesting idea, i tried it but it doesnt seem to solve the issue, maybe the header is not being set from middleware
Log the header on the page see if it is set?
KoratOP
So this is the flow,

Lets say we change the pagination from first page to second or we navigate to different pages.

Middleware is hit (the jwt token is as it is shown)

The access_token has expired (before refresh token is a log before sending a request to refresh token with the currente refresh_token)

After that we hit fetch wrapper which changes the token perfectly but at the same time as you can see in the hit fetch wrapper there is another call to that
endpoint (this time is using the old access_token which triggers the invalid_token error)

I can show the code too if needed but im not sure whats causing this behaviour
So the token in after refresh token is wrong? Where is it coming from?
KoratOP
Is taking the last token instead of the new one,

My thought is that there are multiple calls to client happening all at once.

First call to /refresh-token
Second call to /users

this means that the new token isnt passed to the second call but the old one , and in my case the token has already expired.

Only subsequent calls will be using the new token.

Ive had similar issues with refresh token via next auth.

Similiar to this https://github.com/nextauthjs/next-auth/discussions/3940
@Ray So the token in after refresh token is wrong? Where is it coming from?
KoratOP
im taking the token from cookies inside client.ts aka the wrapper
KoratOP
I cant control the order can I,

I use the fetch wrapper for every query/mutation whether is serverside or clientside
I thought you use the wrapper on server side only? Since its using the cookies() function
The order doesn’t matter as long as the middleware pass the correct token
KoratOP
Actually yes because i use server actions for everything, so even in client it will call a server action and ive also marked the wrapper with ‘use server’ but i dont think thats needed
I wanted to ask you, how do you manage authentication in your projects?

Have you ever had a case in next app router where the jwt token would be issued from an external server?
Yes
Middleware + cookies
KoratOP
No nextjs?
i mean next auth ?
I would love to see how you handle those stuffs because for me only token rotation is not working
@Ray how do you fetch the data? I think the fetch on the page should happen after the token is passed from middleware?
KoratOP
how would we pass the token from middleware to fetch wrapper ?
@Korat how would we pass the token from middleware to fetch wrapper ?
like this
KoratOP
This is what I came up with after testing a lot of cases, seems to work as it should now but cant really trust it haha
Thanks for your time once more man
KoratOP
I would like to get some feedback from you if possible
@Korat This is what I came up with after testing a lot of cases, seems to work as it should now but cant really trust it haha
look good but how you get the token from getBearerToken() or is it hard coded?
KoratOP
its a util, just reading from cookies
A weird thing happening is when refresh token is expired, i would need to redirect on /login page but sometimes it is sometimes is not
I tried this after deleting cookies return NextResponse.redirect(new URL(/${localePrefix}, request.url));
but its causing a weird infinite loop
@Korat I tried this after deleting cookies return NextResponse.redirect(new URL(`/${localePrefix}`, request.url)); but its causing a weird infinite loop
you need to do this
const res = NextResponse.redirect()
res.cookies.delete()
return res
if you just return NextResponse.redirect(), it doesn't delete the cookie
so when the next request come in, it goes to the condition again and causing the loop
KoratOP
NextResponse.redirect asks for a absolute url?
KoratOP
What do i fill it with in my case, i tried redirecting to /en but its saying that the url is malformed
new URL("/en", req.url)
KoratOP
if (
      new Date().getTime() > new Date(session.refreshTokenExpireTime).getTime()
    ) {
      console.log('Refresh token expired, logging out');
      const res = NextResponse.redirect(request.nextUrl);
      res.cookies.delete(AUTH_SECRET);
      return res;
    }
This seems to work nice
Sorry for questions but im not sure how NextResponse flow in middleware works excatly
what flow? the middlware is finished when you return a response
and the response will go to the page or route
KoratOP
Oh okay, i understand now

I just wasn't sure what was causing the loop with this code
if (
new Date().getTime() > new Date(session.refreshTokenExpireTime).getTime()
) {
console.log('Refresh token expired, logging out');
res.cookies.delete(AUTH_SECRET);
return NextResponse.redirect(new URL(/${localePrefix}, request.url));
}
final code looks like this

if (
      new Date().getTime() > new Date(session.refreshTokenExpireTime).getTime()
    ) {
      console.log('Refresh token expired, logging out');
      const response = NextResponse.redirect(
        new URL(`/${localePrefix}`, request.url),
      );
      response.cookies.delete(AUTH_SECRET);

      return response;
    }


Which is redirecting correctly now, i think i understand because we need to create a new response
as per your feedback ❤️
KoratOP
Could you elaborate what was happening, why wasnt the cookie deleted in the first example ? is it because returning NextResponse doesnt know that the variable res above has the cookie or ?
NextResponse.redirect() is creating a new response
you were deleting the cookie in the variable of res
so the header didn't send back to the browser
KoratOP
okay, thanks for the info man,

I think i have a final version now to use it everywhere now thanks to you, maybe someone else can benefit too from this conversation, its not that easy to build our own auth in next xD
Btw one case that i've noticed in next auth is run the middleware when application is focused on chrome , is that something difficult to do on my own?
Run the middleware on app focus
hmm, what do you mean
KoratOP
For example if im in another tab and i switch to the application to run the middleware, maybe react query helps with that using refetchOnWindowFocus
KoratOP
Yeah exactly, react query executes a server action -> executes middleware which handles the refresh token
KoratOP
Yeahh, ill keep that on my mind, gotta refactor the code now finally hahaha