Skip to content

Commit 7973f7b

Browse files
Enhance token expiration handling. Implement a double-check mechanism to verify token validity before logging out the user. Avoiding removing the user on token renewal with multiple tab
1 parent 5b76530 commit 7973f7b

File tree

1 file changed

+40
-16
lines changed

1 file changed

+40
-16
lines changed

shell-ui/src/auth/AuthProvider.tsx

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)