reactjsazure-ad-msalmsal.jsmsal-react

How to redirect a user to login page if token has expired using "React AZURE MSAL SSO"?


Functionality: ReactJS MSAL SSO login, logout, refresh token and redirect to login page once token has expired.

Issue: Unable to redirect to login if the token has expired (using MSAL SSO for ReactJS).

Reference taken from this link - How to handle token expiry in azure msal react?

The below code works great for login, logout, token refresh. However, unable to redirect to login if the token has expired: -

App.js: -

import { MsalProvider } from "@azure/msal-react";
import MainContent from "./views/MainContent";
import "./App.css";

const App = ({ instance }) => {
  return (
    <MsalProvider instance={instance}>
      <MainContent></MainContent>
    </MsalProvider>
  );
};

export default App;

MainContent.js: -

import React, { useEffect } from "react";
import {
  AuthenticatedTemplate,
  useMsal,
  UnauthenticatedTemplate,
} from "@azure/msal-react";
import { useShallow } from "zustand/react/shallow";
import useAuthStore from "../store/authStore";
import { jwtDecode } from "jwt-decode";
import AppRoutes from "../routes/AppRoutes";
import checkUserPermissions from "../utils/checkUserPermissions";
import AutoSignIn from "./AutoSignIn";
import { InteractionRequiredAuthError } from "@azure/msal-browser";

const MainContent = () => {
  const { instance } = useMsal();
  const activeAccount = instance.getActiveAccount();
  const REFRESH_THRESHOLD = 600; // 10 mins (60 seconds * 10 mins = 600)

  const { setLoginState } = useAuthStore(
    useShallow((state) => ({
      setLoginState: state.setLoginState,
    }))
  );

  const { isLoggedOut } = useAuthStore(
    useShallow((state) => ({
      isLoggedOut: state.isLoggedOut,
    }))
  );

  const saveActiveToken = (token) => {
    if (token) {
      sessionStorage.setItem("access-token", token);
    }
  };

  if (activeAccount?.idToken && typeof activeAccount?.idToken != "undefined") {
    saveActiveToken(activeAccount?.idToken);
  }

  const handleLogoutRedirect = () => {
    instance
      .logoutRedirect()
      .catch((error) => console.log("logout error: ", error));

      console.log(instance.getAllAccounts());
      debugger
    };

  const refreshToken = async () => {
    await instance.acquireTokenSilent(activeAccount).catch(async (error) => {
      if (error instanceof InteractionRequiredAuthError) {
        await instance.acquireTokenRedirect(activeAccount);
      }
    });
    saveActiveToken(activeAccount?.idToken);
  };

  useEffect(() => {
    if (isLoggedOut) handleLogoutRedirect();
  }, [isLoggedOut]);

  useEffect(() => {
    setInterval(() => {
      try {
        const token = sessionStorage.getItem("access-token");
        if (typeof token != "undefined" && token != "") {
          const decodedToken = jwtDecode(token);
          const currentTime = Math.floor(Date.now() / 1000); // Current time in seconds
          const timeUntilExpiry = decodedToken.exp - currentTime;
          if (timeUntilExpiry <= REFRESH_THRESHOLD) {
            refreshToken();
          }
        }
      } catch (err) {
        console.log("Printing setInterval() Error: ", err);
      }
    }, 60000);
  }, []);

  return (
    <div>
      <AuthenticatedTemplate>
        {activeAccount ? <AppRoutes /> : null}
      </AuthenticatedTemplate>
      <UnauthenticatedTemplate>
        <AutoSignIn instance={instance} />
      </UnauthenticatedTemplate>
    </div>
  );
};

export default MainContent;

Solution

  • https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/token-lifetimes.md#avoiding-interactive-interruptions-in-the-middle-of-a-users-session

    var request = {
        scopes: ["Mail.Read"],
        account: currentAccount,
        forceRefresh: true
        refreshTokenExpirationOffsetSeconds: 7200 // 2 hours * 60 minutes * 60 seconds = 7200 seconds
    };
    
    const tokenResponse = await msalInstance.acquireTokenSilent(request).catch(async (error) => {
        if (error instanceof InteractionRequiredAuthError) {
            // fallback to interaction when silent call fails
            await msalInstance.acquireTokenRedirect(request);
        }
    });
    

    add this function into useEffect and update the component

    import React, { useEffect } from "react";
    import { useMsal } from "@azure/msal-react";
    import { InteractionRequiredAuthError } from "@azure/msal-browser";
    
    const YourComponent = () => {
      const { instance, accounts } = useMsal();
    
      useEffect(() => {
        const checkTokenExpiration = async () => {
          if (accounts.length > 0) {
            const currentAccount = accounts[0];
            const request = {
              scopes: ["Mail.Read"],
              account: currentAccount,
              forceRefresh: true, // Forces to refresh token
              refreshTokenExpirationOffsetSeconds: 7200, // 2 hours
            };
    
            try {
              // Try to acquire a token silently
              const tokenResponse = await instance.acquireTokenSilent(request);
              console.log("Token acquired:", tokenResponse);
            } catch (error) {
              if (error instanceof InteractionRequiredAuthError) {
                // Fallback to interactive token acquisition when silent call fails
                await instance.acquireTokenRedirect(request);
              } else {
                console.error("Error acquiring token silently:", error);
              }
            }
          }
        };
    
        checkTokenExpiration();
      }, [accounts, instance]);
    
      return <div>{/* Your component content */}</div>;
    };
    
    export default YourComponent;