Middleware + Refresh JWT token
Unanswered
Sun bear posted this in #help-forum
Sun bearOP
im facing a nasty race condition when handling refresh tokens in middleware since they run in parrarrel for server components how can one get around this any sugggestions posibbly a better place to run the jwt refreshes ? thanks in advance looking to hear back from you guys.
110 Replies
Pacific sand lance
where exactly do you get race condition? in nextjs middleware is executed once before each matched request so you should be fine
Paper wasp
agreed - we use a middleware for this, and it only runs once for each route regardless of the amount of server components and async-o-rama in the page rendering
Sun bearOP
interesting
can i maybe elaborate a little more on my setu[?
@Paper wasp @Pacific sand lance
Pacific sand lance
sure
Sun bearOP
so im working with a seperate backend api hitting endpoints to retrive the session tokens and storing them with a signed cookie with JOSE on the frontend. thats the setup for auth, then in the middleware i check if the session is expired or about to expire and i try to refresh it,
im using react query for alot of the data fetching, but also api endpoints for protected api endpoints that i just proxy to the backend so i can read the session etc on the server.
im using react query for alot of the data fetching, but also api endpoints for protected api endpoints that i just proxy to the backend so i can read the session etc on the server.
Pacific sand lance
with almost 1;1 setup im refreshing tokens in middleware (as its impossible to update cookies from within RSCs)
and in axios interceptor i check if response status is 401 and call was made client side - then i call server action which updates cookie
Sun bearOP
could i maybe show my middleware at the moment
Pacific sand lance
sure
Sun bearOP
Pacific sand lance
could you show
refreshSessionIfNeeded
?Sun bearOP
yess
really this whole file has the important parts cant really miss out any details for clairty
@Pacific sand lance could you show `refreshSessionIfNeeded`?
Sun bearOP
i appreacite you by the way
atm i have this lock thing setup but its not really working tbh so could just ignore that for now
Pacific sand lance
does your refresh flow issue now access+refresh pair?
Sun bearOP
yeah so here you can see it you mean api returns new pairs yeah?
Pacific sand lance
yes
Sun bearOP
export async function refreshTokens(
userId: RefreshTokenPayload['userId'],
refreshToken: RefreshTokenPayload['refreshToken']
): Promise<RefreshTokenResponse | null> {
// Guard against missing API_BASE_URL environment variable
if (!process.env.API_BASE_URL) {
throw new Error(
'API_BASE_URL environment variable is required but not set'
);
}
// Build secure API URL without sensitive data in query parameters
const apiUrl = `${process.env.API_BASE_URL}/token/refresh`;
try {
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
},
body: JSON.stringify({ userId, refreshToken }),
});
const responseData: RefreshTokenResponse = await response.json();
if (responseData.code && responseData.code > HTTP_ERROR_THRESHOLD) {
logger.error('Failed to refresh tokens', {
status: response.status,
statusText: response.statusText,
error: safeJoinErrors(responseData.errors, ', '),
userId,
});
return null;
}
const { token, refreshToken: newRefreshToken } = responseData;
if (!token) {
return null;
}
if (!newRefreshToken) {
return null;
}
return {
token,
refreshToken: newRefreshToken,
};
} catch (error) {
logger.error('Token refresh request failed', {
error: error instanceof Error ? error.message : 'Unknown error',
userId,
});
return null;
}
}
Pacific sand lance
ok so you have to update both current request and response cookies
current request to ensure subsequent api calls will be called with fresh access token and response so they are updated in browser
Sun bearOP
i tihnk you are getting somewhre so could you point out which area i should focus
in the code
Pacific sand lance
all within middleware
just return both access token and refresh token to caller
then do sth like
req.cookies.set("<name>", <access token>);
req.cookies.set("<name>", <refresh token>);
const res = NextResponse.next({ request: req });
res.cookies.set("<name>", <access token>);
res.cookies.set("<name>", <refresh token>);
return res;
Sun bearOP
export async function updateSessionWithNewTokens(
userId: string,
token: string,
refreshToken: string
): Promise<void> {
const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days in milliseconds
// decode the token
const decodedToken = decodeJWTUnsafe(token);
if (!decodedToken) {
throw new Error('Invalid token');
}
const accessTokenExpiresAt = new Date((decodedToken?.exp ?? 0) * 1000);
const session = await encrypt({
userId,
expiresAt,
accessToken: token,
accessTokenExpiresAt,
refreshToken,
});
const cookieStore = await cookies();
cookieStore.set('session', session, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
expires: expiresAt,
sameSite: 'lax',
path: '/',
});
}
i had this function did you see it
Pacific sand lance
yes
but still you have to patch both current request (cookie header) and response (set-cookie header) that will be produced
i tried different things for hours back with nextjs 14
Sun bearOP
interseting i think you pin pointed the issue
is it to much to ask if you could provide the snippet
for hte middleware udpate i should try
Pacific sand lance
can be done here i guess
just return cookies so they are accessible after
await refreshSessionIfNeeded()
call@Pacific sand lance then do sth like
ts
req.cookies.set("<name>", <access token>);
req.cookies.set("<name>", <refresh token>);
const res = NextResponse.next({ request: req });
res.cookies.set("<name>", <access token>);
res.cookies.set("<name>", <refresh token>);
return res;
Pacific sand lance
then do sth like this
where
req
is 1st middleware function argument@Pacific sand lance then do sth like
ts
req.cookies.set("<name>", <access token>);
req.cookies.set("<name>", <refresh token>);
const res = NextResponse.next({ request: req });
res.cookies.set("<name>", <access token>);
res.cookies.set("<name>", <refresh token>);
return res;
Pacific sand lance
this mess is necessary to provide fresh tokens for current requests and to patch cookies in browser
atleast i don't anything that's easier do
Sun bearOP
yeah this is been driving me nuts
haha
but i think you found the problem
Pacific sand lance
spent hours on this back then
Sun bearOP
so if the refresh was successful
i set those
above
that you did
Pacific sand lance
yes
you update
req.cookies
to ensure current request will be processed with fresh tokens and res.cookies
to ensure tokens are updated in browserSun bearOP
crazy the docs never highlighted this lol.
okay
do you have the exactly line i could put it in
to not screw it up lol
sorry man but i truly appreacite ur help
for real
Pacific sand lance
either after
or combine with next if statement
if (!refreshSuccess) {
// Refresh failed, redirect to sign-in page
return NextResponse.redirect(new URL('/sign-in', req.nextUrl));
}
or combine with next if statement
to ensure you redirect user to dashboard if tokens were successfully refreshed
Sun bearOP
yeah so this is the current setup // Refresh session if access token is expired or expires within 5 minutes
if (session?.userId) {
const refreshSuccess = await refreshSessionIfNeeded();
if (!refreshSuccess) {
// Refresh failed, redirect to sign-in page
return NextResponse.redirect(new URL('/sign-in', req.nextUrl));
}
}
// 5. Redirect to /dashboard if the user is authenticated
if (
isPublicRoute &&
session?.userId &&
!req.nextUrl.pathname.startsWith('/dashboard')
) {
return NextResponse.redirect(
new URL(DEFAULT_DASHBOARD_REDIRECT_PATH, req.nextUrl)
);
}
return NextResponse.next();
@Pacific sand lance
Pacific sand lance
if (session?.userId) {
const refreshSuccess = await refreshSessionIfNeeded();
if (!refreshSuccess) {
// Refresh failed, redirect to sign-in page
return NextResponse.redirect(new URL('/sign-in', req.nextUrl));
}
req.cookies.set('session', <NEW SESSION TOKEN>);
const res = NextResponse.next({ request: req });
res.cookies.set('session', <NEW SESSION TOKEN>);
return res; // it's not redirected to dashboard, adjust as necessary
}
// 5. Redirect to /dashboard if the user is authenticated
if (
isPublicRoute &&
session?.userId &&
!req.nextUrl.pathname.startsWith('/dashboard')
) {
return NextResponse.redirect(
new URL(DEFAULT_DASHBOARD_REDIRECT_PATH, req.nextUrl)
);
}
return NextResponse.next();
Sun bearOP
ur the GOAT
ill try it out
big shoutout to you bro
Pacific sand lance
also, if you allow users to have multiple sessions this might produce errors
Sun bearOP
yeah that crap
is gone
that was me trying to fix the problem
Pacific sand lance
as same users with different session will be assigned same lock (assuming multiple sessions would be refreshed at once)
Sun bearOP
how does this look
just updated it
Pacific sand lance
seems fine
Sun bearOP
sweet
ill give it a shot thank you brother!
Pacific sand lance
if it works, you may combine it with redirect to dashboard that's below (as of now it won;t be performed)
but idk if NextResponse.redirect can be used here, in my flow i redirect from protected routes to
/auth/sign-in
so it wasn't an issueSun bearOP
i mean i prob hvea simlar setup as you
i can reorgainze it
to work like yours
so with the current setup whats the caviat
like curios to know ur thinking here
Pacific sand lance
mine looks like this
within this if i refresh tokens and on error i redirect user to
/auth/sign-in
Sun bearOP
got you
and with my current setup what could happen
just curios
Pacific sand lance
1. user is on protected route with no session -> redirect to
2. rotation if necessary -> request is processed with new session. you may need to handle rotation error (redirect to
3. if user is on public route and has session -> redirect to dashboard
4. none of above -> handle request
/sign-in
2. rotation if necessary -> request is processed with new session. you may need to handle rotation error (redirect to
/sign-in
) and on success redirect to dashboard (if user is on public route)3. if user is on public route and has session -> redirect to dashboard
4. none of above -> handle request
Sun bearOP
so i just tested out by just not having an expire date so i can just refresh alot
and its doing okay
but when i tried rendering lots of pages at once it broke a little
could be a dev thing tough
@Sun bear but when i tried rendering lots of pages at once it broke a little
Pacific sand lance
what do you mean by tha
rendering lots of pages
Sun bearOP
like loading routes
Sun bearOP
Heyy @Pacific sand lance
Pushed it in prod yesterday been testing out it's working fairly well
I don't think it's still 100% fixed but there is noticably better persistence howver maybe not yet fully working
I read some articles and reasarch seems people are also encorting this issue
Saw something about fetch API being used instead of ajax interceptor or something
Pacific sand lance
i can't tell if fetch is the problem, but i'm using axios
Sun bearOP
interesting
do you reccomend using axios if so why im actually curios i have been using fetch since i started my web dev carerrer looking to hear from the other side haha.
@Sun bear do you reccomend using axios if so why im actually curios i have been using fetch since i started my web dev carerrer looking to hear from the other side haha.
American Chinchilla
Theyre both pretty similar except axios is a library while fetch is built in. In addition axios has interceptors and just handles lots of the manual things that is done using fetch
It depends on the app and project's needs but I personally always just use fetch
less dependencies, and also when using with react query since it solves race conditions.
For server side data , we use a mix of debouce and retries for race conditions along with abort controller in hooks, and idempotent api
If cant use react query^ which is the ideal solution
Sun bearOP
Yeah react query is king