Back

Refresh Token Flow Implementation in MERN Stack Application: A Step-by-Step Guide

Last updated on 15 Mar, 2023

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.

Let's get started

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.

Handle Multiple simultaneous API requests

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 Technologies 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.

about author

Hitesh Agja

My name is Hitesh Agja, and I have over 12 years of experience in the industry. I am constantly driven by my excitement and passion for learning new things, whether they are technical or not. I believe that life is all about continuous learning and progress, and I strive to bring that mindset to everything I do.

Let's talkhire -button