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
?
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
TOKEN_CHECK_INTERVAL
) to reduce the frequency of token checks.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.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
Here are the tokens generated in regular intervals: Local Stoarge