In today's digital age, security is one of the biggest concerns for web applications. One of the most popular authentication mechanisms is the token-based authentication system. This system involves the use of access tokens and refresh tokens to ensure secure communication between the client and server. In this blog post, we will explore how to implement the refresh token flow in a MERN stack application.
Here is code snippet in a MERN stack application that implements a refresh token flow:
Server-side code (Node.js and Express):
First, install the required dependencies:
npm install jsonwebtoken
npm install express-jwt
In your server.js file, set up the Express app with JSON parsing middleware and add the following code:
const jwt = require('jsonwebtoken');
const expressJwt = require('express-jwt');
// define secret key for JWT tokens
const jwtSecret = 'mySecretKey';
// define expiration times for access and refresh tokens
const accessTokenExpirationTime = '15m';
const refreshTokenExpirationTime = '7d';
// set up route for generating new tokens
app.post('/token', (req, res) => {
// get refresh token from request body
const { refreshToken } = req.body;
// if refresh token is not provided, send error response
if (!refreshToken) {
return res.status(401).json({ message: 'Refresh token is required' });
}
// verify refresh token
jwt.verify(refreshToken, jwtSecret, (err, decoded) => {
if (err) {
return res.status(403).json({ message: 'Invalid refresh token' });
}
// generate new access token
const accessToken = jwt.sign({ userId: decoded.userId }, jwtSecret, { expiresIn: accessTokenExpirationTime });
// send new access token
return res.json({ accessToken });
});
});
// set up middleware to verify access token
app.use(expressJwt({ secret: jwtSecret, algorithms: ['HS256'] }));
// set up routes that require authentication
app.get('/protected-route', (req, res) => {
// send response for authenticated user
res.json({ message: 'Authenticated user' });
});
Client-side code (React):
In your React component, add the following code:
import axios from 'axios';
// define function to get new access token using refresh token
const getNewAccessToken = async (refreshToken) => {
try {
const response = await axios.post('/token', { refreshToken });
return response.data.accessToken;
} catch (error) {
console.error(error);
throw error;
}
};
// define function to make authenticated request with access token
const makeAuthenticatedRequest = async (accessToken) => {
try {
const response = await axios.get('/protected-route', {
headers: {
Authorization: "Bearer ${accessToken}",
},
});
return response.data;
} catch (error) {
console.error(error);
throw error;
}
};
// in your component, use these functions to make authenticated requests
const MyComponent = () => {
const [data, setData] = useState(null);
const [accessToken, setAccessToken] = useState(null);
const [refreshToken, setRefreshToken] = useState(null);
// on component mount, get access and refresh tokens from server
useEffect(() => {
const fetchTokens = async () => {
try {
const response = await axios.post('/login', { email, password });
setAccessToken(response.data.accessToken);
setRefreshToken(response.data.refreshToken);
} catch (error) {
console.error(error);
}
};
fetchTokens();
}, []);
// on access token expiration, get new access token using refresh token and make authenticated request
useEffect(() => {
const fetchNewAccessToken = async () => {
try {
const newAccessToken = await getNewAccessToken(refreshToken);
setAccessToken(newAccessToken);
} catch (error) {
console.error(error);
}
};
if (accessToken) {
const { exp } = jwt.decode(accessToken);
if (Date.now() >= exp * 1000) {
fetchNewAccessToken();
}
}
}, [accessToken, refreshToken]);
// make authenticated request with access token
useEffect(() => {
const fetchData = async () => {
try {
const response = await makeAuthenticatedRequest(accessToken);
setData(response);
} catch (error) {
console.error(error);
}
};
if (accessToken) {
fetchData();
}
}, [accessToken]);
return (
// render component with fetched data
);
};
Now, we need to create a middleware to check if the access token has expired. If the access token has expired, we need to use the refresh token to generate a new access token. Here's how we can implement the middleware:
const jwt = require('jsonwebtoken');
const { User } = require('../models/user.model');
const { generateAccessToken, generateRefreshToken } = require('../utils/auth');
const checkAuth = async (req, res, next) => {
try {
const authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
const token = authHeader.split(' ')[1];
const decodedToken = jwt.verify(token, process.env.JWT_SECRET);
// Check if access token has expired
if (decodedToken.exp <= Date.now() / 1000) { const user=await User.findById(decodedToken.sub);
// Check if user exists and has a refresh token
if (!user || !user.refreshToken) { throw new Error('Unauthorized'); }
// Verify refresh token and generate new access token
jwt.verify(user.refreshToken, process.env.JWT_SECRET, (err, decoded)=> {
if (err) {
throw new Error('Unauthorized');
}
const accessToken = generateAccessToken(user.id);
res.setHeader('Authorization', 'Bearer ' + accessToken);
next();
});
} else {
next();
}
} else {
throw new Error('Unauthorized');
}
} catch (error) {
res.status(401).json({ message: error.message });
}
};
This middleware first checks if the Authorization header exists and starts with "Bearer". If it does, it extracts the token from the header and verifies it using the JWT library. If the token has expired, it checks if the user exists and has a refresh token. If the user has a refresh token, it verifies the refresh token and generates a new access token using the generateAccessToken function from the auth utility. It then sets the Authorization header with the new access token and calls the next middleware. If the token has not expired, it simply calls the next middleware. If the Authorization header does not exist or does not start with "Bearer", it throws an error with a 401 status code.
Finally, we need to use the checkAuth middleware in our routes that require authentication:
const express = require('express');
const router = express.Router();
const { checkAuth } = require('../middlewares/checkAuth');
router.get('/protected', checkAuth, (req, res) => {
res.json({ message: 'This is a protected route' });
});
module.exports = router;
This route is now protected and requires a valid access token to access. The checkAuth middleware will automatically generate a new access token if the current access token has expired.
To avoid making multiple requests for a new refresh token, we can implement a token refresh interceptor in our axios instance. This interceptor will intercept every request and check if the access token has expired. If it has, it will request a new access token using the refresh token and then retry the original request.
Here's how we can implement this in our MERN stack application:
Client-side code (React):
import axios from 'axios';
// define axios instance
const axiosInstance = axios.create({
baseURL: '/api',
});
// define function to get new access token using refresh token
const getNewAccessToken = async (refreshToken) => {
try {
const response = await axiosInstance.post('/token', { refreshToken });
return response.data.accessToken;
} catch (error) {
console.error(error);
throw error;
}
};
// define function to make authenticated request with access token
const makeAuthenticatedRequest = async (accessToken) => {
try {
const response = await axiosInstance.get('/protected-route', {
headers: {
Authorization: "Bearer ${accessToken}",
},
});
return response.data;
} catch (error) {
console.error(error);
throw error;
}
};
// add interceptor to axios instance
let isRefreshing = false;
let refreshSubscribers = [];
axiosInstance.interceptors.response.use((response) => response, async (error) => {
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
// if refreshing is already in progress, add original request to refresh subscribers list
return new Promise((resolve) => {
refreshSubscribers.push((accessToken) => {
originalRequest.headers.Authorization = "Bearer ${accessToken}";
resolve(axiosInstance(originalRequest));
});
});
}
isRefreshing = true;
try {
const newAccessToken = await getNewAccessToken(refreshToken);
setAccessToken(newAccessToken);
originalRequest.headers.Authorization = "Bearer ${newAccessToken}";
// retry the original request and resolve all refresh subscribers
const response = await axiosInstance(originalRequest);
refreshSubscribers.forEach((subscriber) => subscriber(newAccessToken));
refreshSubscribers = [];
return response;
} catch (error) {
console.error(error);
throw error;
} finally {
isRefreshing = false;
}
}
return Promise.reject(error);
},
);
In conclusion, implementing the refresh token flow is an important aspect of securing MERN stack applications. By using refresh tokens, we can ensure that users remain authenticated for a longer period of time without having to constantly provide their credentials. In this approach, the client-side code handles the refreshing of the access token when it expires, by making a request to the server using a refresh token.
On the server-side, we must implement the necessary routes and middleware to handle the initial authentication, as well as the refreshing of the access token. It is also important to properly validate and verify tokens to ensure that they have not been tampered with.
By following these steps, we can create a secure and seamless user experience for our MERN stack applications.
3Brain Technolabs can provide expertise in implementing secure authentication and authorization flows, including refresh token flows, in your MERN stack application. Our team of experienced developers can ensure the highest level of security for your application, while also providing a seamless user experience. Contact us to learn more about how we can help you.