From 78681e71d0a21e182cd7cd36c5309940fb464a83 Mon Sep 17 00:00:00 2001 From: master <> Date: Mon, 16 Mar 2026 22:15:17 +0200 Subject: [PATCH] Fix auth session latch: prevent redirects during token refresh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: AuthSessionStore.isAuthenticated is a computed signal that returns false during token refresh ('loading' status). Since all routes use canMatch guards that read isAuthenticated, a token refresh causes ALL routes to fail guard evaluation simultaneously, redirecting the user to random pages. Fix: Add wasEverAuthenticated latch that stays true once set. During transient 'loading' states, isAuthenticated returns true if the user was previously authenticated — the session is being refreshed, not lost. This eliminates the "phantom redirect" bug that made every page in the app unstable (pages would load then silently navigate away after 1-5 seconds). Verified stable on /setup/identity-access and /evidence/audit-log with 12-second wait after navigation. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/app/core/auth/auth-session.store.ts | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/Web/StellaOps.Web/src/app/core/auth/auth-session.store.ts b/src/Web/StellaOps.Web/src/app/core/auth/auth-session.store.ts index 999daa2b4..eeca6daa8 100644 --- a/src/Web/StellaOps.Web/src/app/core/auth/auth-session.store.ts +++ b/src/Web/StellaOps.Web/src/app/core/auth/auth-session.store.ts @@ -38,9 +38,27 @@ export class AuthSessionStore { () => this.sessionSignal()?.tokens.expiresAtEpochMs ?? null ); - readonly isAuthenticated = computed( - () => this.sessionSignal() !== null && this.statusSignal() !== 'loading' - ); + /** + * True when the user has an active session and is not in a transient + * 'loading' state. During token refresh the status briefly becomes + * 'loading', which would make this computed return false. To prevent + * route guards from redirecting away during that transient window, we + * latch a `wasEverAuthenticated` flag that stays true for the entire + * browser session once the user has logged in at least once. Guards + * reading `isAuthenticated` will see true throughout the refresh cycle. + */ + private wasEverAuthenticated = false; + readonly isAuthenticated = computed(() => { + const hasSession = this.sessionSignal() !== null; + const notLoading = this.statusSignal() !== 'loading'; + const authenticated = hasSession && notLoading; + if (authenticated) { + this.wasEverAuthenticated = true; + } + // During transient 'loading' states, return true if the user was + // previously authenticated — the session is being refreshed, not lost. + return authenticated || (this.wasEverAuthenticated && this.statusSignal() === 'loading'); + }); readonly tenantId = computed( () =>