skip to Main Content

I was learning how to create a login system using JWT tokens. And as we know, this strategy involves creating two tokens: access_token and refresh_token.

Regarding the refresh_token, it is saved in a cookie and is managed server-side. The access_token is managed by the user (front-end application), where most tutorials on the internet save it in localStorage.

After some research, I came to the conclusion that the best alternative is to save this token (access_token) within the application’s memory.

To try to achieve the same result, I created a context called AuthContext.js:

import { useState, createContext, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import axios from 'axios';

export const AuthContext = createContext({});

const AuthProvider = ({ children }) => {
    const [accessToken, setAccessToken] = useState(null);
    
    const signIn = async (email, password) => {
        setLoadingAuth(true);
        try {
            const { data } = await axios.post('http://localhost/signIn', { email: email, password: password });
            setAccessToken(data.accessToken);
            setLoadingAuth(false);
            navigate('/dashboard');
        } catch (error) {
            alert(error.response.data.message);
            setLoadingAuth(false);
            console.error(error);
        }
    }
    
    return(
        <AuthContext.Provider value={{ accessToken, userInfo, loadingAuth, loading, signIn, logout }}>
            {children}
        </AuthContext.Provider>
    );
}

export default AuthProvider;

Which is being imported into App.js together with the React Router DOM library:

export default function App() {
    return (
        <BrowserRouter>
            <AuthProvider>
                <RouterApp />
            </AuthProvider>
        </BrowserRouter>
    );
}

The problem is that after the user refreshes the browser screen, the access_token that was stored within the accessToken state is LOST.

Obviously because it is being saved in memory. However, as my application didn’t close, I believe the accessToken should still store that value, right?

To get around this problem, I’m thinking about creating a SINGLETON, that is, a class that will use the object.freeze function in order to persist the accessToken for some time.

Would this be the best alternative?

2

Answers


  1. You can use local storage

    // save the access_token
    
    localstorage.setItem("access_token", "access_token value")
    
    // read the access_token
    
    localstorage.getItem("access_token")
    Login or Signup to reply.
  2. To store the access token in the application memory, you could create a class for managing it, such as:

    authenticationManager.js

    let accessToken = "";
    
    export default class AuthenticationManager {
      setAccessToken(s) {
        accessToken = s;
      }
    
      getAccessToken() {
        return accessToken;
      }
    }
    
    let authenticationManager = new AuthenticationManager();
    
    export const getAuthenticationManager = () => {
      return authenticationManager;
    };
    

    And then after a successful login, you can set the access token. Something similar to:

    authenticationService.js

    import { getAuthenticationManager } from "./authenticationManager";
    
    ...
    
    export async function login(loginUserRequest) {
      const response = await authenticationApi.login(loginUserRequest);
    
      if (response.ok) {
        const userTokenResponse = await response.json();
    
        // Setting access token
        getAuthenticationManager().setAccessToken(userTokenResponse.access_token);
      }
    }
    
    ...
    

    If you have a class that performs API requests, you can check if the access token has been set before performing a request. If it hasn’t, you can use the refresh token to obtain a new access token before executing the original request. Something similar to:

    apiClient.js

    import { getAuthenticationManager } from "./authenticationManager";
    
    const SERVER_ADDRESS = "https://server.com";
    const MAX_REFRESH_ACCESS_TOKEN_ATTEMPTS = 2;
    
    export default class ApiClient {
      constructor() {
        this.getAccessToken = () =>
          getAuthenticationManager()?.getAccessToken() || "";
        this.setAccessToken = (accessToken) =>
          getAuthenticationManager()?.setAccessToken(accessToken);
      }
    
      /**
       * Refreshes the access token for the user.
       * If the refresh is successful, updates the access token.
       */
      async refreshAccessToken() {
        const response = await fetch(`${SERVER_ADDRESS}/api/refresh_access_token`, {
          method: "GET",
          credentials: "include",
        });
    
        let accessToken = "";
        if (response.ok) {
          const responseBody = await response.json();
          this.setAccessToken(responseBody.access_token);
        }
      }
    
      /**
       * Sends a REST request to the server.
       * If the access token is not available or has expired, it will be refreshed before sending the request.
       * If the response status is 401 (Unauthorized), it will attempt to refresh the access token and retry the request.
       * @param uri - The URI of the request.
       * @param options - The options for the request.
       * @param attempts - The number of attempts to refresh the access token if needed. Defaults to MAX_REFRESH_ACCESS_TOKEN_ATTEMPTS.
       * @returns A Promise that resolves to the response of the request.
       */
      async restRequest(
        uri,
        options,
        attempts = MAX_REFRESH_ACCESS_TOKEN_ATTEMPTS
      ) {
        if (
          this.getAccessToken() === "" &&
          attempts === MAX_REFRESH_ACCESS_TOKEN_ATTEMPTS
        ) {
          await this.refreshAccessToken();
        }
    
        let response = await fetch(SERVER_ADDRESS + uri, {
          ...options,
          credentials: "include",
          headers: {
            ...options.headers,
            Authorization: "Bearer " + this.getAccessToken(),
          },
        });
    
        /**
         * If the response status is 401 (Unauthorized), an attempt will be made
         * to refresh the access token and retry the request. The number of attempts
         * to refresh the access token is decremented by 1.
         */
        if (response.status === 401 && attempts > 0) {
          await this.refreshAccessToken();
          response = await this.restRequest(uri, options, attempts - 1);
        }
    
        return response;
      }
    }
    

    As long as your refresh token cookie has been set to persist between browser sessions, it can be used to refresh the access token every time a new session is created (or the browser is refreshed).

    If the access token stored in the application memory has expired, it will be refreshed when it’s used to make an API request.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search