Skip to content

Commit d35d11a

Browse files
Jonattan Infanteclaude
andcommitted
WEB-956: share authenticated session across tabs and windows
Currently AuthenticationService persists the user credentials in sessionStorage by default (and only opts into localStorage when the optional Remember Me checkbox is checked). sessionStorage is scoped to a single browser tab/window, so 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. 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. - The `storage` field 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 controls which Storage is used in the browser. - A BroadcastChannel named `mifosXAuth` is created lazily (with a feature-detect fallback for older browsers). On login the service broadcasts a `{ type: 'login' }` message; on logout it broadcasts `{ type: 'logout' }`. Other tabs listening on the same channel rehydrate or wipe their state accordingly without waiting for a reload — matching what users expect from modern SaaS UIs. Verified end-to-end against the isolated docker-compose stack: master: click on a Mifos link while logged in → re-login forced fix: click on a Mifos link while logged in → session preserved fix: logout in tab A → tab B reacts live Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 06c9ae5 commit d35d11a

1 file changed

Lines changed: 55 additions & 8 deletions

File tree

src/app/core/authentication/authentication.service.ts

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,17 @@ export class AuthenticationService {
5858
/** Denotes whether the user credentials should persist through sessions. */
5959
private rememberMe = false;
6060
/**
61-
* Denotes the type of storage:
61+
* Credentials are always persisted in localStorage so the authenticated
62+
* session is shared across browser tabs and windows of the same origin.
63+
* sessionStorage would scope the credentials to a single tab, which forces
64+
* an unnecessary re-login when the user opens any internal link from an
65+
* external program (email, chat, bookmark, target="_blank", etc.).
6266
*
63-
* Session Storage: User credentials should not persist through sessions.
64-
*
65-
* Local Storage: User credentials should persist through sessions.
67+
* The `rememberMe` flag still controls the token expiration policy on the
68+
* backend; on logout (or when credentials are cleared explicitly) the
69+
* storage is wiped from both localStorage and sessionStorage.
6670
*/
67-
private storage: Storage = sessionStorage;
71+
private storage: Storage = localStorage;
6872
private credentials: Credentials;
6973
private dialogShown = false;
7074
private authMode: AuthMode = AuthMode.Basic;
@@ -77,6 +81,17 @@ export class AuthenticationService {
7781
/** Key to store two factor authentication token in storage. */
7882
private readonly twoFactorAuthenticationTokenStorageKey = 'mifosXTwoFactorAuthenticationToken';
7983

84+
/**
85+
* Broadcast channel used to synchronise login/logout events across browser
86+
* tabs and windows of the same origin. Mirrors the pattern used by
87+
* `@supabase/auth-js` (see supabase/auth-js GoTrueClient.ts) so that a
88+
* logout in any tab propagates to the others without waiting for a page
89+
* reload, and a login in a new tab updates already-open ones too.
90+
* Falls back gracefully when BroadcastChannel is unavailable.
91+
*/
92+
private readonly broadcastChannel: BroadcastChannel | null =
93+
typeof BroadcastChannel !== 'undefined' ? new BroadcastChannel('mifosXAuth') : null;
94+
8095
/**
8196
* Initializes the type of storage and authorization headers depending on whether
8297
* credentials are presently in storage or not.
@@ -93,6 +108,30 @@ export class AuthenticationService {
93108
}
94109

95110
this.restoreSession();
111+
this.listenForCrossTabAuthEvents();
112+
}
113+
114+
/**
115+
* Listens for login/logout events broadcast from other tabs and updates
116+
* the local authentication state accordingly. This keeps the UI in sync
117+
* (e.g. a logout in tab A removes the session from tab B in real-time).
118+
*/
119+
private listenForCrossTabAuthEvents(): void {
120+
if (!this.broadcastChannel) return;
121+
this.broadcastChannel.onmessage = (event: MessageEvent<{ type: 'login' | 'logout' }>) => {
122+
if (event.data?.type === 'logout' && this.userLoggedIn$.getValue()) {
123+
// Mirror the logout effects without re-broadcasting (avoid loops).
124+
this.authenticationInterceptor.removeAuthorization();
125+
[
126+
localStorage,
127+
sessionStorage
128+
].forEach((store) => store.removeItem(this.credentialsStorageKey));
129+
this.userLoggedIn$.next(false);
130+
} else if (event.data?.type === 'login' && !this.userLoggedIn$.getValue()) {
131+
// Another tab logged in: rehydrate from shared storage.
132+
this.restoreSession();
133+
}
134+
};
96135
}
97136

98137
/**
@@ -220,7 +259,10 @@ export class AuthenticationService {
220259
}
221260

222261
this.rememberMe = environment.enableRememberMe ? (loginContext?.remember ?? false) : false;
223-
this.storage = this.rememberMe ? localStorage : sessionStorage;
262+
// Always use localStorage so the authenticated session is visible
263+
// to any tab/window opened against the same origin (links from
264+
// email, chat, bookmarks, target="_blank", new browser windows, ...).
265+
this.storage = localStorage;
224266

225267
// Basic Auth: Direct authentication with Fineract
226268
return this.http
@@ -374,6 +416,7 @@ export class AuthenticationService {
374416
this.setCredentials();
375417
this.resetDialog();
376418
this.userLoggedIn$.next(false);
419+
this.broadcastChannel?.postMessage({ type: 'logout' });
377420

378421
if (this.authMode === AuthMode.OIDC) {
379422
// OIDC: Use library to handle logout (redirects to OIDC provider)
@@ -435,11 +478,15 @@ export class AuthenticationService {
435478
*/
436479
private setCredentials(credentials?: Credentials): void {
437480
if (credentials) {
481+
const wasLoggedIn = this.userLoggedIn$.getValue();
438482
credentials.rememberMe = this.rememberMe;
439-
// Make sure we're using the correct storage based on rememberMe value
440-
this.storage = credentials.rememberMe ? localStorage : sessionStorage;
483+
// Credentials are always written to localStorage so the session is
484+
// shared across tabs/windows of the same origin (see class doc).
485+
this.storage = localStorage;
441486
this.oauthService.setStorage(this.storage);
442487
this.storage.setItem(this.credentialsStorageKey, JSON.stringify(credentials));
488+
// Notify other tabs only on a NEW login (not on every credential refresh).
489+
if (!wasLoggedIn) this.broadcastChannel?.postMessage({ type: 'login' });
443490
} else {
444491
// Clear credentials from both storage types to ensure complete logout
445492
[

0 commit comments

Comments
 (0)