1- import React , { createContext , useContext , useEffect , useState , useCallback } from 'react' ;
1+ import React , { createContext , useContext , useEffect , useState , useCallback , useRef } from 'react' ;
22import Keycloak from 'keycloak-js' ;
33import LinearProgress from '@mui/material/LinearProgress' ;
44import { API_URL } from '../constants' ;
@@ -21,8 +21,16 @@ export const KeycloakProvider = ({ children, onTokens }) => {
2121 const [ loading , setLoading ] = useState ( true ) ;
2222 const [ error , setError ] = useState ( null ) ;
2323 const { dispatch } = useAuth ( ) ;
24+ const initialized = useRef ( false ) ;
25+ const refreshTimerRef = useRef ( null ) ;
2426
2527 const initKeycloak = useCallback ( async ( ) => {
28+ // Prevent re-initialization
29+ if ( initialized . current ) {
30+ return ;
31+ }
32+ initialized . current = true ;
33+
2634 try {
2735 setLoading ( true ) ;
2836 setError ( null ) ;
@@ -47,23 +55,68 @@ export const KeycloakProvider = ({ children, onTokens }) => {
4755 }
4856
4957 // Create Keycloak instance
50- const keycloakInstance = new Keycloak ( {
58+ let keycloakInstance = new Keycloak ( {
5159 realm : result . realm ,
5260 url : result . url ,
5361 clientId : result . clientId ,
5462 } ) ;
5563
5664 // Initialize Keycloak
57- const authenticated = await keycloakInstance . init ( {
58- pkceMethod : 'S256' ,
59- checkLoginIframe : false ,
60- } ) ;
65+ // Disable iframe check on localhost to avoid CORS/iframe issues
66+ const isLocalhost =
67+ window . location . hostname . includes ( 'localhost' ) ||
68+ window . location . hostname . includes ( '127.0.0.1' ) ;
69+
70+ let authenticated ;
71+
72+ if ( isLocalhost ) {
73+ console . log ( 'Running on localhost - disabling iframe check' ) ;
74+ authenticated = await keycloakInstance . init ( {
75+ pkceMethod : 'S256' ,
76+ checkLoginIframe : false ,
77+ } ) ;
78+ } else {
79+ try {
80+ authenticated = await keycloakInstance . init ( {
81+ pkceMethod : 'S256' ,
82+ checkLoginIframe : true ,
83+ checkLoginIframeInterval : 30 , // Check every 30 seconds
84+ messageReceiveTimeout : 10000 , // Wait up to 10 seconds for iframe
85+ } ) ;
86+ } catch ( initError ) {
87+ console . warn (
88+ 'Keycloak iframe initialization failed, falling back to token-only mode:' ,
89+ initError ,
90+ ) ;
91+ // Create a new instance for fallback since Keycloak can only be initialized once
92+ keycloakInstance = new Keycloak ( {
93+ realm : result . realm ,
94+ url : result . url ,
95+ clientId : result . clientId ,
96+ } ) ;
97+ authenticated = await keycloakInstance . init ( {
98+ pkceMethod : 'S256' ,
99+ checkLoginIframe : false ,
100+ } ) ;
101+ }
102+ }
61103
62104 if ( authenticated ) {
63105 // Extract user data from Keycloak token
64106 const tokenParsed = keycloakInstance . tokenParsed ;
65107
66108 if ( tokenParsed ) {
109+ // Debug token expiration times
110+ const now = Math . floor ( Date . now ( ) / 1000 ) ;
111+ const tokenExp = tokenParsed . exp ;
112+ const tokenMinutesLeft = Math . floor ( ( tokenExp - now ) / 60 ) ;
113+ console . log ( '=== TOKEN DEBUG INFO ===' ) ;
114+ console . log ( 'Current time:' , new Date ( now * 1000 ) . toLocaleTimeString ( ) ) ;
115+ console . log ( 'Token expires at:' , new Date ( tokenExp * 1000 ) . toLocaleTimeString ( ) ) ;
116+ console . log ( 'Token valid for:' , tokenMinutesLeft , 'minutes' ) ;
117+ console . log ( 'checkLoginIframe enabled:' , ! keycloakInstance . checkLoginIframe === false ) ;
118+ console . log ( '========================' ) ;
119+
67120 // Check different possible locations for roles
68121 const realmRoles = tokenParsed . realm_access ?. roles || [ ] ;
69122 const clientRoles = tokenParsed . resource_access ?. [ tokenParsed . aud ] ?. roles || [ ] ;
@@ -131,6 +184,46 @@ export const KeycloakProvider = ({ children, onTokens }) => {
131184 setKeycloak ( keycloakInstance ) ;
132185 setAuthenticated ( authenticated ) ;
133186
187+ // Set up global fetch interceptor for 401 errors
188+ if ( authenticated ) {
189+ const originalFetch = window . fetch ;
190+ window . fetch = async ( ...args ) => {
191+ try {
192+ const response = await originalFetch ( ...args ) ;
193+
194+ // If we get 401 Unauthorized, session has expired
195+ if ( response . status === 401 && storage . get ( 'TOKEN' ) ) {
196+ console . error ( '401 Unauthorized - clearing session' ) ;
197+ storage . remove ( 'TOKEN' ) ;
198+ setAuthenticated ( false ) ;
199+ alert ( 'Your session has expired. Please login again.' ) ;
200+ keycloakInstance . login ( {
201+ redirectUri : window . location . href ,
202+ } ) ;
203+ return response ;
204+ }
205+
206+ return response ;
207+ } catch ( error ) {
208+ // Handle network errors (CORS, network failure, etc.)
209+ // These often happen when token is expired and server rejects the request
210+ if (
211+ storage . get ( 'TOKEN' ) &&
212+ ( error . message . includes ( 'Failed to fetch' ) || error . message . includes ( 'CORS' ) )
213+ ) {
214+ console . error ( 'Network error with expired token - clearing session' ) ;
215+ storage . remove ( 'TOKEN' ) ;
216+ setAuthenticated ( false ) ;
217+ alert ( 'Your session has expired. Please login again.' ) ;
218+ keycloakInstance . login ( {
219+ redirectUri : window . location . href ,
220+ } ) ;
221+ }
222+ throw error ; // Re-throw for other types of errors
223+ }
224+ } ;
225+ }
226+
134227 // Set up token refresh
135228 if ( authenticated ) {
136229 const tokens = {
@@ -143,15 +236,13 @@ export const KeycloakProvider = ({ children, onTokens }) => {
143236 onTokens ( tokens ) ;
144237 }
145238
146- // Also call user info loading here if needed
147- // You can pass a getUserInfo callback through props
148-
149- // Set up automatic token refresh
150- keycloakInstance . onTokenExpired = ( ) => {
239+ // Helper function to handle token refresh
240+ const refreshToken = ( ) => {
151241 keycloakInstance
152- . updateToken ( 30 )
242+ . updateToken ( 60 ) // Refresh if token expires in less than 60 seconds
153243 . then ( ( refreshed ) => {
154244 if ( refreshed ) {
245+ console . log ( 'Token refreshed successfully' ) ;
155246 const newTokens = {
156247 token : keycloakInstance . token ,
157248 refreshToken : keycloakInstance . refreshToken ,
@@ -160,13 +251,77 @@ export const KeycloakProvider = ({ children, onTokens }) => {
160251 if ( onTokens ) {
161252 onTokens ( newTokens ) ;
162253 }
254+ storage . set ( 'TOKEN' , keycloakInstance . token ) ;
255+
256+ // Schedule next refresh after successful refresh
257+ scheduleTokenRefresh ( ) ;
258+ } else {
259+ console . log ( 'Token not refreshed, still valid' ) ;
260+ // Token still valid, check again in 10 seconds
261+ // This prevents tight loops when token is close to but not within refresh threshold
262+ refreshTimerRef . current = setTimeout ( ( ) => {
263+ refreshToken ( ) ;
264+ } , 10000 ) ;
163265 }
164266 } )
165- . catch ( ( ) => {
166- console . error ( 'Failed to refresh token' ) ;
167- keycloakInstance . login ( ) ;
267+ . catch ( ( error ) => {
268+ console . error ( 'Failed to refresh token - session expired' , error ) ;
269+ // Clear stored token to prevent further API calls with expired token
270+ storage . remove ( 'TOKEN' ) ;
271+ // Alert user and redirect to login
272+ alert ( 'Your session has expired. You will be redirected to login.' ) ;
273+ keycloakInstance . login ( {
274+ redirectUri : window . location . href ,
275+ } ) ;
168276 } ) ;
169277 } ;
278+
279+ // Schedule token refresh before expiration
280+ const scheduleTokenRefresh = ( ) => {
281+ // Clear any existing timer
282+ if ( refreshTimerRef . current ) {
283+ clearTimeout ( refreshTimerRef . current ) ;
284+ }
285+
286+ // Calculate when to refresh (e.g., 1 minute before expiration)
287+ const tokenParsed = keycloakInstance . tokenParsed ;
288+ if ( tokenParsed && tokenParsed . exp ) {
289+ const now = Math . floor ( Date . now ( ) / 1000 ) ;
290+ const tokenExp = tokenParsed . exp ;
291+ const timeUntilExpiry = tokenExp - now ;
292+
293+ // Refresh 60 seconds before expiration
294+ // Add minimum delay of 10 seconds to prevent tight loops
295+ const refreshTime = Math . max ( 10000 , ( timeUntilExpiry - 60 ) * 1000 ) ;
296+
297+ console . log ( `Next token refresh check in ${ Math . floor ( refreshTime / 1000 ) } seconds` ) ;
298+
299+ refreshTimerRef . current = setTimeout ( ( ) => {
300+ refreshToken ( ) ;
301+ } , refreshTime ) ;
302+ }
303+ } ;
304+
305+ // Start the refresh schedule
306+ scheduleTokenRefresh ( ) ;
307+
308+ // Set up automatic token refresh as fallback (in case timer misses)
309+ keycloakInstance . onTokenExpired = ( ) => {
310+ console . warn ( 'Token expired - refreshing immediately (fallback handler)' ) ;
311+ refreshToken ( ) ;
312+ } ;
313+
314+ // Monitor SSO session state when checkLoginIframe is enabled
315+ keycloakInstance . onAuthLogout = ( ) => {
316+ console . log ( 'SSO logout detected' ) ;
317+ if ( refreshTimerRef . current ) {
318+ clearTimeout ( refreshTimerRef . current ) ;
319+ }
320+ storage . remove ( 'TOKEN' ) ;
321+ setAuthenticated ( false ) ;
322+ alert ( 'You have been logged out. Please login again.' ) ;
323+ window . location . reload ( ) ;
324+ } ;
170325 }
171326
172327 // Save environment variables
@@ -178,18 +333,30 @@ export const KeycloakProvider = ({ children, onTokens }) => {
178333 } catch ( err ) {
179334 console . error ( 'Failed to initialize Keycloak:' , err ) ;
180335 setError ( err . message ) ;
336+ // Set a fallback null keycloak to prevent crashes
337+ setKeycloak ( null ) ;
338+ setAuthenticated ( false ) ;
181339 } finally {
182340 setLoading ( false ) ;
183341 }
184342 } , [ onTokens , dispatch ] ) ;
185343
186344 useEffect ( ( ) => {
187345 initKeycloak ( ) ;
346+
347+ // Cleanup function to clear refresh timer
348+ return ( ) => {
349+ if ( refreshTimerRef . current ) {
350+ clearTimeout ( refreshTimerRef . current ) ;
351+ }
352+ } ;
188353 } , [ initKeycloak ] ) ;
189354
190355 const login = useCallback ( ( ) => {
191356 if ( keycloak ) {
192- keycloak . login ( ) ;
357+ keycloak . login ( {
358+ redirectUri : window . location . href ,
359+ } ) ;
193360 }
194361 } , [ keycloak ] ) ;
195362
0 commit comments