You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
WEB-956: share auth session across tabs and preserve deep links
Currently AuthenticationService persists the user credentials in
sessionStorage by default, which is scoped to a single browser tab. As
a result, any link to a Mifos URL opened from outside the current tab
— email, chat, bookmark, target="_blank", a new browser window — boots
into an empty storage and the AuthenticationGuard sends the
already-logged-in user back to /login. Compounding this,
AuthenticationGuard does not propagate the requested URL and
LoginComponent unconditionally navigates to "/" after a successful
login, so the originally requested deep link is permanently lost.
This change adopts the pattern used by `@supabase/auth-js`
(GoTrueClient.ts) and Auth0's SPA SDK: localStorage as the single
source of truth for the session, plus a BroadcastChannel for
cross-tab synchronisation. It also makes the guard forward the
target URL through the login flow.
- AuthenticationService.storage is now localStorage unconditionally,
so the session is visible to every tab/window of the same origin.
The `rememberMe` flag is preserved for the backend token-expiration
policy but no longer chooses the Storage instance.
- getSavedCredentials() prefers localStorage and performs a one-time
migration of any leftover sessionStorage credentials so existing
users do not get stranded after the upgrade.
- A BroadcastChannel named `mifosXAuth` is created lazily (with a
feature-detect fallback). Login broadcasts `{ type: 'login' }`,
logout broadcasts `{ type: 'logout' }`; the listener uses
hadPersistedCredentials to fire the login broadcast even though
onLoginSuccess() flips userLoggedIn$ before setCredentials() runs.
- The cross-tab logout handler now also clears the two-factor token
storage and the two-factor authorization header, mirroring what
logout() does on the active tab.
- AuthenticationGuard.canActivate now accepts RouterStateSnapshot and
forwards state.url as a returnUrl query parameter when the target
is a meaningful route. Trivial targets ("/", "/login") are skipped
to avoid noise and redirect loops.
- LoginComponent reads queryParamMap.get('returnUrl') from the
ActivatedRoute when the authentication success alert fires, and
navigates there via navigateByUrl. Falls back to "/" when no
returnUrl is present.
- Adds unit tests for the guard covering: authenticated pass-through,
deep-link capture, "/" and "/login" being skipped to avoid
loops/noise, stale-params handling, and URLs with query params
plus fragments.
Verified end-to-end against an isolated docker-compose stack
(postgres + fineract + web-app):
before: /#/clients -> /#/login -> /#/home (bug)
after: /#/clients -> /#/login?returnUrl=/clients -> /#/clients
cross-tab logout: tab A logs out -> tab B reacts live.
Addresses CodeRabbit review feedback:
- getSavedCredentials() now treats localStorage as authoritative and
migrates legacy sessionStorage credentials once.
- Cross-tab logout handler also clears 2FA state.
- Login broadcast is gated on persisted credentials, not on
userLoggedIn$, so a fresh login is correctly announced to other
tabs even though onLoginSuccess() sets userLoggedIn$ first.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
0 commit comments