I am working on an e-commerce project using the MERN stack and implementing JWT-based authentication. Here’s the tech stack:
Frontend: Next.js (App Router) with RTK Query for state management.
Backend: Node.js with Express.js.
Database: MySQL.
I use the following flow for authentication:
On login, I store the access_token (short-lived, 15 mins) and refresh_token (long-lived, 7 days) in secure, HTTP-only cookies.
For protected routes, I use an isAuthenticated middleware on the backend to verify the access token from cookies.
Here’s the middleware I’m using:
export const isAuthenticated = (req, res, next) => {
try {
const access_token = req.cookies.user_access_token;
if (!access_token) {
throw new Error('Login to access this resource');
}
const decode = jwt.verify(access_token, process.env.ACCESS_TOKEN_SECRET);
if (!decode) {
throw new Error('Invalid JWT token');
}
req.user = decode.user;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
console.log('Access token expired');
}
res.status(401).json({ message: 'Please log in again' });
}
};
If the access_token expires, users are forced to log in again, which is not ideal. Instead, I want to implement a token refresh mechanism such that:
If the access_token expires, the client sends a request to refresh the token using the refresh_token stored in cookies.
After refreshing the token, the original request should automatically retry.
I’m using RTK Query on the frontend to handle API requests and state management. Here’s the RTK Query base setup:
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
const apiSlice = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: '/api',
credentials: 'include', // Include cookies in requests
}),
endpoints: (builder) => ({
getCategories: builder.query({
query: () => '/categories',
}),
createCategory: builder.mutation({
query: (category) => ({
url: '/categories',
method: 'POST',
body: category,
}),
}),
}),
});
export const { useGetCategoriesQuery, useCreateCategoryMutation } = apiSlice;
export default apiSlice;
When the access_token expires, I want RTK Query to automatically refresh the token by calling a /auth/refresh-token endpoint on the backend and retry the failed request.
What is the best way to implement this functionality in the backend and integrate it with RTK Query on the frontend?
export const refreshAccessToken = (req, res) => {
try {
const refreshToken = req.cookies.user_refresh_token;
if (!refreshToken) {
return res.status(401).json({ message: 'Refresh token missing' });
}
const decoded = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET);
const newAccessToken = jwt.sign(
{ user: decoded.user },
process.env.ACCESS_TOKEN_SECRET,
{ expiresIn: '15m' }
);
res.cookie('user_access_token', newAccessToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 15 * 60 * 1000, // 15 minutes
});
return res.status(200).json({ success: true });
} catch (error) {
return res.status(403).json({ message: 'Invalid refresh token' });
}
};
What I Tried:
I’ve looked into:
-
Adding a custom baseQuery in RTK Query to handle token refresh and retry failed requests.
-
Updating the isAuthenticated middleware to attempt refreshing the token before responding with a 401.
Question:
-
How can I configure RTK Query to intercept a 401 Unauthorized response, refresh the access token using the /auth/refresh-token endpoint, and retry the failed request?
-
Is my current backend implementation for the refresh token secure and aligned with best practices?
Any guidance or code examples would be greatly appreciated!