diff --git a/frontend/App.tsx b/frontend/App.tsx
index 2d9c29d..c496ae4 100644
--- a/frontend/App.tsx
+++ b/frontend/App.tsx
@@ -2,8 +2,12 @@
import React from 'react';
import { RouterProvider } from 'react-router-dom';
import { router } from './router';
+import { useTokenRenewal } from './hooks/useTokenRenewal';
const App: React.FC = () => {
+ // Initialize token renewal service
+ useTokenRenewal();
+
return ;
};
diff --git a/frontend/authConfig.ts b/frontend/authConfig.ts
index 913158b..592af98 100644
--- a/frontend/authConfig.ts
+++ b/frontend/authConfig.ts
@@ -14,7 +14,7 @@ export const msalConfig: Configuration = {
},
cache: {
cacheLocation: "localStorage", // This configures where your cache will be stored
- storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge
+ storeAuthStateInCookie: true, // Enable cookies for better session persistence across browser sessions
},
};
diff --git a/frontend/hooks/useTokenRenewal.ts b/frontend/hooks/useTokenRenewal.ts
new file mode 100644
index 0000000..6471195
--- /dev/null
+++ b/frontend/hooks/useTokenRenewal.ts
@@ -0,0 +1,32 @@
+import { useEffect } from 'react';
+import { useIsAuthenticated } from '@azure/msal-react';
+import { tokenRenewalService } from '../services/tokenRenewalService';
+import { USE_MSAL_AUTH } from '../app.config';
+
+/**
+ * Custom hook to manage token renewal for authenticated users
+ * Automatically starts/stops the renewal service based on authentication state
+ */
+export const useTokenRenewal = (): void => {
+ const isAuthenticated = useIsAuthenticated();
+
+ useEffect(() => {
+ // Only manage token renewal when using MSAL auth
+ if (!USE_MSAL_AUTH) {
+ return;
+ }
+
+ if (isAuthenticated) {
+ console.log('User authenticated, starting token renewal service');
+ tokenRenewalService.start();
+ } else {
+ console.log('User not authenticated, stopping token renewal service');
+ tokenRenewalService.stop();
+ }
+
+ // Cleanup on unmount
+ return () => {
+ tokenRenewalService.stop();
+ };
+ }, [isAuthenticated]);
+};
\ No newline at end of file
diff --git a/frontend/services/tokenRenewalService.ts b/frontend/services/tokenRenewalService.ts
new file mode 100644
index 0000000..e84f78e
--- /dev/null
+++ b/frontend/services/tokenRenewalService.ts
@@ -0,0 +1,123 @@
+import { msalInstance, loginRequest } from '../authConfig';
+import { InteractionRequiredAuthError } from '@azure/msal-browser';
+
+class TokenRenewalService {
+ private renewalInterval: NodeJS.Timeout | null = null;
+ private isRenewing = false;
+ private readonly RENEWAL_INTERVAL = 30 * 60 * 1000; // 30 minutes
+ /**
+ * Start the token renewal service
+ */
+ public start(): void {
+ if (this.renewalInterval) {
+ this.stop(); // Clear existing interval
+ }
+
+ console.log('Starting token renewal service...');
+
+ // Initial token check
+ this.renewTokenIfNeeded();
+
+ // Set up periodic renewal
+ this.renewalInterval = setInterval(() => {
+ this.renewTokenIfNeeded();
+ }, this.RENEWAL_INTERVAL);
+ }
+
+ /**
+ * Stop the token renewal service
+ */
+ public stop(): void {
+ if (this.renewalInterval) {
+ clearInterval(this.renewalInterval);
+ this.renewalInterval = null;
+ console.log('Token renewal service stopped');
+ }
+ }
+
+ /**
+ * Manually trigger token renewal
+ */
+ public async renewToken(): Promise {
+ return this.renewTokenIfNeeded();
+ }
+
+ /**
+ * Check if token needs renewal and renew if necessary
+ */
+ private async renewTokenIfNeeded(): Promise {
+ if (this.isRenewing) {
+ console.log('Token renewal already in progress, skipping...');
+ return false;
+ }
+
+ try {
+ this.isRenewing = true;
+
+ const accounts = msalInstance.getAllAccounts();
+ if (accounts.length === 0) {
+ console.log('No accounts found, cannot renew token');
+ return false;
+ }
+
+ const account = accounts[0];
+
+ // Check if token is close to expiry
+ if (!this.isTokenNearExpiry(account)) {
+ console.log('Token is still valid, no renewal needed');
+ return true;
+ }
+
+ console.log('Token is near expiry, attempting silent renewal...');
+
+ // Attempt silent token renewal
+ await msalInstance.acquireTokenSilent({
+ ...loginRequest,
+ account: account,
+ forceRefresh: true, // Force refresh to get new token
+ });
+
+ console.log('Token renewed successfully via silent request');
+ return true;
+
+ } catch (error) {
+ console.warn('Silent token renewal failed:', error);
+
+ if (error instanceof InteractionRequiredAuthError) {
+ console.log('Interactive authentication required - user will need to sign in again on next request');
+ // Don't trigger popup automatically as it might be disruptive
+ // Let the user-triggered request handle the interactive flow
+ }
+
+ return false;
+ } finally {
+ this.isRenewing = false;
+ }
+ }
+
+ /**
+ * Check if the current token is near expiry
+ */
+ private isTokenNearExpiry(_account: any): boolean {
+ // Always attempt renewal for proactive refreshing
+ // MSAL handles token expiry checks internally, so we'll rely on forceRefresh
+ return true;
+ }
+
+ /**
+ * Get the renewal interval in milliseconds
+ */
+ public getRenewalInterval(): number {
+ return this.RENEWAL_INTERVAL;
+ }
+
+ /**
+ * Check if the service is currently running
+ */
+ public isRunning(): boolean {
+ return this.renewalInterval !== null;
+ }
+}
+
+// Export a singleton instance
+export const tokenRenewalService = new TokenRenewalService();
\ No newline at end of file
diff --git a/frontend/services/userDataService.ts b/frontend/services/userDataService.ts
index cf56eda..a8b2aaa 100644
--- a/frontend/services/userDataService.ts
+++ b/frontend/services/userDataService.ts
@@ -2,7 +2,7 @@ import { SavedQuery } from '../types';
import { msalInstance, loginRequest } from '../authConfig';
import { USE_MSAL_AUTH, API_BASE_URL } from '../app.config';
import { mockDelay, mockSavedQueries } from './mockData';
-import { getAuthErrorMessage, isAuthenticationExpiredError } from '../utils/authErrorHandler';
+import { getAuthErrorMessage, isAuthenticationExpiredError, isRecoverableAuthError } from '../utils/authErrorHandler';
const getAccessToken = async (): Promise => {
try {
@@ -10,15 +10,26 @@ const getAccessToken = async (): Promise => {
if (accounts.length === 0) {
throw new Error("No signed-in user found.");
}
+
+ // Try silent token acquisition first
const response = await msalInstance.acquireTokenSilent({
...loginRequest,
account: accounts[0],
});
return response.accessToken;
} catch (error) {
- // Handle authentication errors with user-friendly messages
- if (isAuthenticationExpiredError(error)) {
- throw new Error(getAuthErrorMessage(error));
+ // If silent token acquisition fails, check if we can recover with interactive auth
+ if (isRecoverableAuthError(error) || isAuthenticationExpiredError(error)) {
+ try {
+ console.log('Silent token acquisition failed, attempting popup refresh...');
+ // Try popup for token refresh
+ const response = await msalInstance.acquireTokenPopup(loginRequest);
+ return response.accessToken;
+ } catch (popupError) {
+ console.error('Popup token refresh also failed:', popupError);
+ // Only after both silent and popup fail, throw the user-friendly error
+ throw new Error(getAuthErrorMessage(error));
+ }
}
// Re-throw other errors as-is
throw error;
diff --git a/frontend/utils/authErrorHandler.ts b/frontend/utils/authErrorHandler.ts
index b962e89..5d027da 100644
--- a/frontend/utils/authErrorHandler.ts
+++ b/frontend/utils/authErrorHandler.ts
@@ -45,6 +45,26 @@ export const isAuthenticationExpiredError = (error: any): boolean => {
return false;
};
+/**
+ * Check if an error is recoverable through interactive authentication (popup/redirect)
+ */
+export const isRecoverableAuthError = (error: any): boolean => {
+ if (error instanceof InteractionRequiredAuthError) {
+ return true;
+ }
+
+ if (error instanceof AuthError) {
+ const recoverableCodes = [
+ 'interaction_required',
+ 'consent_required',
+ 'login_required'
+ ];
+ return recoverableCodes.some(code => error.errorCode?.includes(code));
+ }
+
+ return false;
+};
+
/**
* Get a user-friendly error message for authentication errors
*/