javascriptreactjsnode.jstypescriptjwt

How can I properly implement refresh token handling on the frontend in a React application?


So I have been trying to create a fullstack app that uses both an Access Token and refresh token, I have had success in past occassion with implementing access tokens but with refresh tokens I'm really struggling.

To give you some context: I have been working on the backend of the app and I have come up with:

//Tokens send with the login of the user:

 const accessToken = jwt.sign(
      {
        userId: foundUsername.rows[0].id_persona,
        username: foundUsername.rows[0].username,
        roles: foundRoles.rows,
      },
      process.env.JWT_SECRET_KEY as string,
      { expiresIn: "5m" }
    );

    const refreshToken = jwt.sign(
      {
        userId: foundUsername.rows[0].id_persona,
        username: foundUsername.rows[0].username,
      },
      process.env.JWT_SECRET_KEY as string,
      {
        expiresIn: "1d",
      }
    );

    res.cookie("refresh_cookie", refreshToken, {
      httpOnly: true,
      secure: false,
      sameSite: "none",
      maxAge: 24 * 60 * 60 * 1000,
    }); 
return res.json({ accessToken });

Now if I am not mistaken the refresh token is supposed to be holded in a local storage while the Refresh one is supposed to be the "cookie".

this is the refresh the "access token" when asked to endpoint:

export const refreshAccess = async (req: Request, res: Response) => {
  // Check if the token exists
  const token = req.cookies["refresh_token"];
  if (!token) {
    return res.status(401).json({ message: "Unauthorized" });
  }

  try {
    const decoded = jwt.verify(
      token,
      process.env.JWT_SECRET_KEY as string
    ) as JwtPayload;
    const result = await pool.query(
      "SELECT id_roles FROM assignated_roles WHERE id_persona = $1",
      [decoded.userId]
    );

    const foundRoles = result.rows.map((row) => row.id_roles);
    const accessToken = jwt.sign(
      {
        userId: decoded.userId,
        username: decoded.username,
        roles: foundRoles,
      },
      process.env.JWT_SECRET_KEY as string,
      { expiresIn: "5m" }
    );

    return res.json({ accessToken });

I am satisfied with these Endpoints. I have tested them and they work... in theory. But here come my big questions that I have been asking myself before even finishing my login.

How am I supposed to reach these endpoints? I was inspired to create and use Refresh/Access token becouse of this post: https://designtechworld.medium.com/how-to-authenticate-using-access-and-refresh-tokens-using-react-js-57756df2d282

Now in said post the way the Access Token is being refreshed its through a timer... That.. Can't be right no? I was thinking about holding the data in a context so I could make protected Routes later on. But in order to do so. I would have to constantly make requests with each click the user make. So I would be constantly asking the DB for information. Which again doesn't seem right.

As you might have noticed I feel frozen becouse I have so many ways I feel like this could be implemented.

I would like some guidance as to what are best practices when it comes to the implementation of Refresh/Access tokens when it comes to the frontend(React).

With that being said, any feedback on how to properly create a working frontend would be really appreciated.

Thank you for your time!


Solution

  • You're on the right track and it seems you really got the point of handling tokens with the frontend.

    Usually when dealing with auth provides they provide two main tokens:

    Each token has different expiration date, for example when dealing with cognito from AWS, the access token has a maximum TTL of 1 hours but the refresh token has a maximum of 10 years of TTL.

    As you mentioned and did, usually the access token should be saved via http-only token by the backend response so that the browser would attach the access token to each request and you wouldn't have any access to that token from the browser's JS.

    About handling the refresh token, there are common practices and one of them is just when doing a login request, the token would be saved in local-storage.

    And afterwards, you can create an interceptor on the frontend that when a request fails due to Authorization expiration, you would make a request to get a new access token with the refresh token and then you retry the original request again.

    The axios library has a really interceptors implementation and you can do something like this:

    export const axiosInstance = axios.create();
    
    axiosInstance.interceptors.response.use(
      async (response) => {
        if (response.statusCode === 401) {
          try {
            const refreshToken = localStorage.getItem('refreshToken');
            await axios.get(
              `/token?refreshToken=${refreshToken}`
            );
            
            return axiosInstance(response.config);
          } catch (error) {
            console.log(error);
          }
        }
    
        return response;
      },
      async (error) => {
        return Promise.reject(error);
      }
    );