Authentication / Refresh token process
Answered
Korat posted this in #help-forum
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.
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
currently, we can't set the cookie from the page due to http streaming
currently, we can't set the cookie from the page due to http streaming
84 Replies
@Korat 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.
its better to handle it in middleware i guess and try to make the fetch wrapper later
currently, we can't set the cookie from the page due to http streaming
currently, we can't set the cookie from the page due to http streaming
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 workKoratOP
req.cookies.set but it showed the same error
this should work
const res = NextResponse.next()
res.cookies.set('cookie', 'value')
return resKoratOP
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
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?
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
I wish there was a way to handle it inside the fetch wrapper
@Korat 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
I don't get it. are you fetching on the page? when a user open a page, it should hit the middleware first?
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
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
and in fetchWrapper
const res = handleI18nRouting(request)
if (needRefresh) {
res.headers.set("Authorization": `Bearer ${newToken}`)
}
return resand 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
@Korat Interesting idea, i tried it but it doesnt seem to solve the issue, maybe the header is not being set from middleware
Are you passing the header to fetchWrapper?
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
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
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
@Korat 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
how do you fetch the data? I think the fetch on the page should happen after the token is passed from middleware?
KoratOP
I cant control the order can I,
I use the fetch wrapper for every query/mutation whether is serverside or clientside
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?
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 ?
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(
but its causing a weird infinite loop
/${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 resif you just return
NextResponse.redirect(), it doesn't delete the cookieso 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(
}
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
Which is redirecting correctly now, i think i understand because we need to create a new response
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 â¤ï¸
@Korat 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));
}
const response = NextResponse.redirect(new URL(/${localePrefix}, request.url));
response.cookies.delete(AUTH_SECRET);
return responseKoratOP
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
resso 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
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
yea react-query/swr
KoratOP
Yeah exactly, react query executes a server action -> executes middleware which handles the refresh token
@Korat Yeah exactly, react query executes a server action -> executes middleware which handles the refresh token
you could set a interval with react-query/swr too
KoratOP
Yeahh, ill keep that on my mind, gotta refactor the code now finally hahaha