Re-authenticate instead of going blank when the session is lost#3042
Re-authenticate instead of going blank when the session is lost#3042dvdstelt wants to merge 2 commits into
Conversation
App.vue renders the whole app behind shouldShowApp (authEnabled, isAuthenticated, isAnonymousRoute). When the access token expired and silent renewal failed, isAuthenticated flipped to false and the app rendered nothing, requiring a manual browser refresh to recover. Watch for the session being lost while running and re-trigger authentication: with a live identity-provider session this is a silent redirect round-trip; otherwise the user lands on the provider's login page. Skipped on anonymous routes and while a sign-in is already in progress.
5bdb083 to
a9b62fe
Compare
| // renewal failed), re-trigger authentication instead of rendering a blank page. With a live | ||
| // identity-provider session this is a silent redirect round-trip; otherwise the user lands on | ||
| // the provider's login page. Without this the app renders nothing until a manual refresh. | ||
| watch([authEnabled, isAuthenticated, isAnonymousRoute], ([enabled, authenticated, anonymous]) => { |
There was a problem hiding this comment.
I'm thinking that all this logic, including the isAnonymousRoute, isn't actually the responsibility of the App page and should instead be in the authStore. Pulling in @warwickschroeder for comment.
There was a problem hiding this comment.
Agreed, it probably shouldn't live here. I mean it'll work but its better handled in the auth domain. We are already handling OIDC events in useAuth.ts. These could be used to pickup on the "token exired" or "silent renewal error" events and then reauthenticate. This way we arent "watching" a proxy state, but rather reacting to the actual OIDC event.
…watch Addresses review feedback on #3042: the re-authentication logic doesn't belong in App.vue, and watching the isAuthenticated proxy state is indirect. Move recovery into the auth domain: useAuth now reacts to the addAccessTokenExpired and addSilentRenewError OIDC events directly and re-authenticates via signinRedirect, guarded against re-entrancy and the logged-out route. This also distinguishes session loss from an intentional logout (addUserUnloaded), which must not re-trigger authentication, and recovers proactively on a silent-renewal error instead of waiting for the token to fully expire. App.vue is now display-only. Its spec covers the layout gating; useAuth.spec covers the event-driven recovery.
There was a problem hiding this comment.
Given we did not touch App.vue, there is no need for this test
| function reauthenticate() { | ||
| if (authStore.isAuthenticating) { | ||
| return; | ||
| } | ||
| if (window.location.hash === `#${routeLinks.loggedOut}`) { | ||
| return; | ||
| } | ||
| authStore.setAuthenticating(true); | ||
| userManager?.signinRedirect().catch((error) => { | ||
| authStore.setAuthenticating(false); | ||
| logger.error("Re-authentication after session loss failed:", error); | ||
| }); | ||
| } |
There was a problem hiding this comment.
| function reauthenticate() { | |
| if (authStore.isAuthenticating) { | |
| return; | |
| } | |
| if (window.location.hash === `#${routeLinks.loggedOut}`) { | |
| return; | |
| } | |
| authStore.setAuthenticating(true); | |
| userManager?.signinRedirect().catch((error) => { | |
| authStore.setAuthenticating(false); | |
| logger.error("Re-authentication after session loss failed:", error); | |
| }); | |
| } | |
| async function reauthenticate() { | |
| if (authStore.isAuthenticating) { | |
| return; | |
| } | |
| if (window.location.hash === `#${routeLinks.loggedOut}`) { | |
| return; | |
| } | |
| authStore.setAuthenticating(true); | |
| try { | |
| await userManager?.signinRedirect(); | |
| } catch (error) { | |
| logger.error("Re-authentication after session loss failed:", error); | |
| authStore.setAuthError(error instanceof Error ? error.message : "Re-authentication after session loss failed"); | |
| } finally { | |
| authStore.setAuthenticating(false); | |
| } | |
| } |
| userManager.events.addAccessTokenExpired(() => { | ||
| authStore.clearToken(); | ||
| reauthenticate(); |
There was a problem hiding this comment.
| userManager.events.addAccessTokenExpired(() => { | |
| authStore.clearToken(); | |
| reauthenticate(); | |
| userManager.events.addAccessTokenExpired(async () => { | |
| authStore.clearToken(); | |
| await reauthenticate(); |
| userManager.events.addSilentRenewError((error) => { | ||
| logger.error("Silent renew error:", error); | ||
| reauthenticate(); | ||
| }); |
There was a problem hiding this comment.
| userManager.events.addSilentRenewError((error) => { | |
| logger.error("Silent renew error:", error); | |
| reauthenticate(); | |
| }); | |
| userManager.events.addSilentRenewError(async (error) => { | |
| logger.error("Silent renew error:", error); | |
| await reauthenticate(); | |
| }); |
PhilBastian
left a comment
There was a problem hiding this comment.
looks good with John's suggested changes
Problem
When OIDC auth is enabled and the access token expires (and silent renewal does
not succeed),
App.vuerenders nothing and the user has to manually refresh thebrowser to recover.
App.vuegates the entire app behindshouldShowApp(authEnabled,isAuthenticated,isAnonymousRoute). On token expiry,oidc-client-tsclearsthe token,
isAuthenticatedflips tofalse, and thatv-ifrenders nothing.Nothing re-triggers authentication, so the page stays blank until a refresh
re-runs the auth flow on mount.
Fix
Watch for the session being lost while the app is running and re-trigger
authentication via the existing
useAuth().authenticate()flow:the user keeps working.
already in progress, to avoid loops.
Tests
App.spec.ts: re-authenticates on token loss; does not while alreadyauthenticating; does not on an anonymous route.
Notes
masterdirectly.identity-provider configuration concern (the provider must grant
offline_accessso a refresh token is issued); ServicePulse already requests it.