@@ -165,27 +165,51 @@ export function useAuth(): {
165165 const auth = useOauth2Auth ( ) ; // todo add support for OAuth2Proxy
166166
167167 const { config } = useShellConfig ( ) ;
168- //Force logout when token is expired or we are missing expires_at claims
168+
169+ const hasExpiredDate = auth . userData ?. expires_at ;
170+ const userIsExpired = auth . userData ?. expired || ! hasExpiredDate ;
171+ const shouldEnableQuery = ! ! (
172+ auth . userData &&
173+ userIsExpired &&
174+ // @ts -expect-error - window.isLoggingOut is a temp flag to prevent multiple logout attempts
175+ ! window . isLoggingOut
176+ ) ;
177+
178+ // Handle token expiration with a double-check mechanism.
179+ //
180+ // Problem: React state (auth.userData) and localStorage can get out of sync.
181+ // When silent renew refreshes the token in localStorage, React state still shows
182+ // the old expired token until the next re-render. This would incorrectly trigger
183+ // a logout even though a valid token exists in localStorage.
184+ //
185+ // Solution: Before removing the user, read directly from localStorage via
186+ // userManager.getUser() to verify the token is actually expired.
169187 useQuery ( {
170188 queryKey : [ 'removeUser' ] ,
171189 queryFn : ( ) => {
172- // This query might be executed when useAuth is rendered simultaneously by 2 different components
173- // react-query is supposed to prevent this but in practice under certain conditions a race condition might trigger it twice
174- // We need to make sure we don't call removeUser twice in this case (which would cause a redirect loop)
175- // @ts -expect-error - FIXME when you are working on it
176- window . loggingOut = true ;
177- return auth . userManager . removeUser ( ) . then ( ( ) => {
178- location . reload ( ) ;
190+ // Prevent concurrent logout attempts from multiple components
191+ // @ts -expect-error - window.isLoggingOut is a temp flag
192+ window . isLoggingOut = true ;
193+
194+ // Double-check expiration against localStorage (source of truth)
195+ return auth . userManager . getUser ( ) . then ( ( user ) => {
196+ const isActuallyExpired = user ?. expired || ! user ?. expires_at ;
197+
198+ if ( isActuallyExpired ) {
199+ // Token is genuinely expired in localStorage - log out
200+ return auth . userManager . removeUser ( ) . then ( ( ) => {
201+ location . reload ( ) ;
202+ } ) ;
203+ }
204+
205+ // Token in localStorage is valid (silent renew succeeded).
206+ // React state will catch up on next render - no action needed.
207+ // @ts -expect-error - reset the flag since we're not actually logging out
208+ window . isLoggingOut = false ;
209+ return Promise . resolve ( ) ;
179210 } ) ;
180211 } ,
181- enabled : ! ! (
182- auth &&
183- auth . userManager &&
184- auth . userData &&
185- ( auth . userData . expired || ! auth . userData . expires_at ) &&
186- // @ts -expect-error - FIXME when you are working on it
187- ! window . loggingOut
188- ) ,
212+ enabled : shouldEnableQuery ,
189213 } ) ;
190214
191215 if ( ! auth || ! auth . userData ) {
0 commit comments