Search code examples
reactjsazureazure-active-directoryazure-ad-msalmsal-react

How to handle token expiry in azure msal react?


I'm using Azure Single Sign-On (SSO) for login in my React application, and I'm encountering issues with handling token expiry and renewal. After a successful login, I obtain a token and save it in the localStorage. However, when the token expires or is about to expire, I want to acquire a new token and update it in the localStorage.

I've created a hook that runs every 3 seconds to check if the token has expired or is about to expire. If it is, I call the acquireTokenSilent method to obtain a new token and update it in the local storage. This process repeats continuously.

Here's the relevant code I've implemented:

import { useEffect, useRef } from 'react';
import { useMsal } from '@azure/msal-react';
import jwt from 'jwt-decode';
import { backendScopeRequest } from '../config/authConfig';
import { useAuthStore } from '../store/auth';

const REFRESH_THRESHOLD = 300; // 5 minutes in seconds

export const useBackendTokenCheckExpirationTime = () => {
  const interval = useRef(null);
  const { instance, accounts } = useMsal();
  const { updateBackendAccessToken, updateLoggedInUserProfile } = useAuthStore();

  const setRefreshTime = () => {
    const backendAccessToken = localStorage.getItem('backendAccessToken');
    if (backendAccessToken) {
      const decodeToken = jwt(backendAccessToken);
      const currentTime = Math.floor(Date.now() / 1000); // Current time in seconds
      const timeUntilExpiry = decodeToken.exp - currentTime - REFRESH_THRESHOLD;
      localStorage.setItem('backendRefreshTime', timeUntilExpiry);
      return timeUntilExpiry;
    }
    return null;
  };

  const handleLogout = () => {};

  const acquireTokenWithRefreshToken = async () => {
    try {
      if (accounts.length && instance) {
        const response = await instance.acquireTokenSilent({
          account: accounts[0],
          ...backendScopeRequest,
        });
        const decodeToken = jwt(response.accessToken);
        localStorage.setItem('backendAccessToken', response.accessToken);
        updateBackendAccessToken(response.accessToken);
        updateLoggedInUserProfile(decodeToken);
        const currentTime = Math.floor(Date.now() / 1000);
        const timeUntilExpiry = decodeToken.exp - currentTime - REFRESH_THRESHOLD;
        localStorage.setItem('backendRefreshTime', timeUntilExpiry);
      }
    } catch (error) {
      console.log('error', error);
      handleLogout();
      // Handle token refresh error
    }
  };

  useEffect(() => {
    interval.current = setInterval(() => {
      const backendRefreshTime = setRefreshTime();
      if (backendRefreshTime !== null && backendRefreshTime <= 0) {
        acquireTokenWithRefreshToken();
      }
    }, 3000);

    return () => clearInterval(interval.current);
  }, []);
};

The issue I'm facing is that the code above doesn't seem to refresh the token correctly. Even when the token has expired, it still uses the old token from localStorage, resulting in a 401 error from the API.

I'm relatively new to Azure SSO, and I'm looking for guidance on how to properly handle token expiry in Azure MSAL React. Specifically, how can I obtain a new token when the old one expires and update it in the localStorage?


Solution

  • Your acquireTokenWithRefreshToken function seems correct. It attempts to silently acquire a new token using the acquireTokenSilent method and then updates the token in local storage. However, you need to make sure this function is called when the token has expired. Below are the changes i have did for hook

    • I've increased the token check interval to 1 minute (TOKEN_CHECK_INTERVAL) to reduce the frequency of token checks.
    • The checkTokenExpiry function now checks if the token is about to expire or has expired based on the REFRESH_THRESHOLD. If it is, it triggers token renewal.
    • I've added a call to checkTokenExpiry immediately after mounting to check token expiry as soon as the component loads.

    Here's an updated version of your useBackendTokenCheckExpirationTime hook:

    import { useEffect, useRef } from  'react';
    import { useMsal } from  '@azure/msal-react';
    import  jwt  from  'jwt-decode';
    import { backendScopeRequest } from  '../src/authConfig';
    import { useAuthStore } from  '../src/storeauth';
    
    const  REFRESH_THRESHOLD  =  300; // 5 minutes in seconds
    const  TOKEN_CHECK_INTERVAL  =  60000; // 1 minute in milliseconds
    
    
    export  const  useBackendTokenCheckExpirationTime  = () => {
    const  interval  =  useRef(null);
    const { instance, accounts } =  useMsal();
    const { updateBackendAccessToken, updateLoggedInUserProfile } =  useAuthStore();
    const  acquireTokenWithRefreshToken  =  async () => { 
    try {  
    if (accounts.length  &&  instance) {
    const  response  =  await  instance.acquireTokenSilent({
    account:  accounts[0],
    });
    const  decodeToken  =  jwt(response.accessToken);
    localStorage.setItem('backendAccessToken', response.accessToken); 
    localStorage.getItem('backendAccessToken')
    updateBackendAccessToken(response.accessToken);
    updateLoggedInUserProfile(decodeToken);
    console.log('Token refreshed');
    console.log('Token renewed:', decodeToken);
    }
    } catch (error) {  
    console.log('Error refreshing token', error);  // Handle token refresh error
    }
    };
    useEffect(() => {
    const  checkTokenExpiry  = () => {
    const  backendAccessToken  =  localStorage.getItem('backendAccessToken');
    if (backendAccessToken) {
    const  decodeToken  =  jwt(backendAccessToken); 
    const  currentTime  =  Math.floor(Date.now() /  1000); // Current time in seconds
    const  timeUntilExpiry  =  decodeToken.exp  -  currentTime; 
    if (timeUntilExpiry  <=  REFRESH_THRESHOLD) {     // Token is about to expire or has expired, refresh it
    acquireTokenWithRefreshToken();
    }
    }
    };
    interval.current  =  setInterval(checkTokenExpiry, TOKEN_CHECK_INTERVAL);
    checkTokenExpiry(); // Check token expiry immediately after mounting     
    return () =>  clearInterval(interval.current);
    }, []);
    return  null; // You might not need to return anything from this hook
    
    };
    

    Result

    enter image description here

    enter image description here

    Here are the tokens generated in regular intervals: enter image description here Local Stoarge enter image description here