Middleware + Refresh JWT token
Unanswered
American black bear posted this in #help-forum
American black 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
Longtail tuna
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
American black bearOP
interesting
can i maybe elaborate a little more on my setu[?
@Paper wasp @Longtail tuna
Longtail tuna
sure
American black 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.
Longtail tuna
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
American black bearOP
could i maybe show my middleware at the moment
Longtail tuna
sure
American black bearOP
Longtail tuna
could you show
refreshSessionIfNeeded?American black bearOP
yess
really this whole file has the important parts cant really miss out any details for clairty
@Longtail tuna could you show `refreshSessionIfNeeded`?
American black 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
Longtail tuna
does your refresh flow issue now access+refresh pair?
American black bearOP
yeah so here you can see it you mean api returns new pairs yeah?
Longtail tuna
yes
American black 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;
}
}Longtail tuna
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
American black bearOP
i tihnk you are getting somewhre so could you point out which area i should focus
in the code
Longtail tuna
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;American black 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
Longtail tuna
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
American black 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
Longtail tuna
can be done here i guess
just return cookies so they are accessible after
await refreshSessionIfNeeded() call@Longtail tuna 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;
Longtail tuna
then do sth like this
where
req is 1st middleware function argument@Longtail tuna 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;
Longtail tuna
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
American black bearOP
yeah this is been driving me nuts
haha
but i think you found the problem
Longtail tuna
spent hours on this back then
American black bearOP
so if the refresh was successful
i set those
above
that you did
Longtail tuna
yes
you update
req.cookies to ensure current request will be processed with fresh tokens and res.cookies to ensure tokens are updated in browserAmerican black 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
Longtail tuna
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
American black 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();@Longtail tuna
Longtail tuna
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();American black bearOP
ur the GOAT
ill try it out
big shoutout to you bro
Longtail tuna
also, if you allow users to have multiple sessions this might produce errors
American black bearOP
yeah that crap
is gone
that was me trying to fix the problem
Longtail tuna
as same users with different session will be assigned same lock (assuming multiple sessions would be refreshed at once)
American black bearOP
how does this look
just updated it
Longtail tuna
seems fine
American black bearOP
sweet
ill give it a shot thank you brother!
Longtail tuna
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 issueAmerican black 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
Longtail tuna
mine looks like this
within this if i refresh tokens and on error i redirect user to
/auth/sign-inAmerican black bearOP
got you
and with my current setup what could happen
just curios
Longtail tuna
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-in2. 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
American black 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
@American black bear but when i tried rendering lots of pages at once it broke a little
Longtail tuna
what do you mean by tha
rendering lots of pagesAmerican black bearOP
like loading routes
American black bearOP
Heyy @Longtail tuna
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
Longtail tuna
i can't tell if fetch is the problem, but i'm using axios
American black 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.
@American black 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
American black bearOP
Yeah react query is king