diff --git a/src/context/SessionContext.tsx b/src/context/SessionContext.tsx index a2eed4e22..4eaf4e47e 100644 --- a/src/context/SessionContext.tsx +++ b/src/context/SessionContext.tsx @@ -1,10 +1,10 @@ -import React, { createContext, useContext } from 'react'; +import React, { createContext, useContext, useEffect, useCallback, useRef } from 'react'; import StatusContext from './StatusContext'; import { BackendApi, useApi } from '../api'; -import { useLocalStorageKeystore } from '../services/LocalStorageKeystore'; +import { KeystoreEvent, useLocalStorageKeystore } from '../services/LocalStorageKeystore'; import type { LocalStorageKeystore } from '../services/LocalStorageKeystore'; - +import keystoreEvents from '../services/keystoreEvents'; type SessionContextValue = { api: BackendApi, @@ -23,20 +23,50 @@ const SessionContext: React.Context = createContext({ export const SessionContextProvider = ({ children }) => { const { isOnline } = useContext(StatusContext); const api = useApi(isOnline); - const keystore = useLocalStorageKeystore(); + const keystore = useLocalStorageKeystore(keystoreEvents); + + // Use a ref to hold a stable reference to the clearSession function + const clearSessionRef = useRef<() => void>(); + + // Memoize clearSession using useCallback + const clearSession = useCallback(async () => { + sessionStorage.setItem('freshLogin', 'true'); + console.log('Clear Session'); + api.clearSession(); + }, [api]); + + // Update the ref whenever clearSession changes + useEffect(() => { + clearSessionRef.current = clearSession; + }, [clearSession]); + + // The close() will dispatch Event CloseSessionTabLocal in order to call the clearSession + const logout = async () => { + await keystore.close(); + }; + + useEffect(() => { + // Handler function that calls the current clearSession function + const handleClearSession = () => { + if (clearSessionRef.current) { + clearSessionRef.current(); + } + }; + + // Add event listener + keystoreEvents.addEventListener(KeystoreEvent.CloseSessionTabLocal, handleClearSession); + + // Cleanup event listener to prevent duplicates + return () => { + keystoreEvents.removeEventListener(KeystoreEvent.CloseSessionTabLocal, handleClearSession); + }; + }, []); const value: SessionContextValue = { api, isLoggedIn: api.isLoggedIn() && keystore.isOpen(), keystore, - logout: async () => { - - // Clear URL parameters - sessionStorage.setItem('freshLogin', 'true'); - api.clearSession(); - await keystore.close(); - - }, + logout, }; return ( diff --git a/src/services/LocalStorageKeystore.ts b/src/services/LocalStorageKeystore.ts index 6033a557d..841cafbd7 100644 --- a/src/services/LocalStorageKeystore.ts +++ b/src/services/LocalStorageKeystore.ts @@ -27,6 +27,11 @@ export type CachedUser = { prfKeys: WebauthnPrfSaltInfo[]; } +export enum KeystoreEvent { + /** This event should be propagated to needed tabs which must clean SessionStorage. */ + CloseSessionTabLocal = 'keystore.closeSessionTabLocal', +} + export type CommitCallback = () => Promise; export interface LocalStorageKeystore { isOpen(): boolean, @@ -75,7 +80,7 @@ export interface LocalStorageKeystore { } /** A stateful wrapper around the keystore module, storing state in the browser's localStorage and sessionStorage. */ -export function useLocalStorageKeystore(): LocalStorageKeystore { +export function useLocalStorageKeystore(eventTarget: EventTarget): LocalStorageKeystore { const [cachedUsers, setCachedUsers,] = useLocalStorage("cachedUsers", []); const [privateData, setPrivateData, clearPrivateData] = useLocalStorage("privateData", null); const [globalUserHandleB64u, setGlobalUserHandleB64u, clearGlobalUserHandleB64u] = useLocalStorage("userHandle", null); @@ -94,21 +99,22 @@ export function useLocalStorageKeystore(): LocalStorageKeystore { } }, [])); - const closeTabLocal = useCallback( - () => { + const closeSessionTabLocal = useCallback( + async (): Promise => { + eventTarget.dispatchEvent(new CustomEvent(KeystoreEvent.CloseSessionTabLocal)); clearSessionStorage(); }, - [clearSessionStorage], + [clearSessionStorage, eventTarget], ); const close = useCallback( async (): Promise => { + console.log('Keystore Close'); await idb.destroy(); clearPrivateData(); clearGlobalUserHandleB64u(); - closeTabLocal(); }, - [closeTabLocal, idb, clearGlobalUserHandleB64u, clearPrivateData], + [idb, clearGlobalUserHandleB64u, clearPrivateData], ); useOnUserInactivity(close, config.INACTIVE_LOGOUT_MILLIS); @@ -132,18 +138,30 @@ export function useLocalStorageKeystore(): LocalStorageKeystore { return cu; } })); + } + }, + [closeSessionTabLocal, privateData, userHandleB64u, globalUserHandleB64u, setCachedUsers], + ); - } else if (!privateData) { - // When user logs out in any tab, log out in all tabs - closeTabLocal(); - - } else if (userHandleB64u && globalUserHandleB64u && (userHandleB64u !== globalUserHandleB64u)) { + useEffect( + () => { + if (userHandleB64u && globalUserHandleB64u && (userHandleB64u !== globalUserHandleB64u)) { // When user logs in in any tab, log out in all other tabs // that are logged in to a different account - closeTabLocal(); + closeSessionTabLocal(); + } + }, + [closeSessionTabLocal, userHandleB64u, globalUserHandleB64u, setCachedUsers], + ); + + useEffect( + () => { + if (!privateData) { + // When user logs out in any tab, log out in all tabs + closeSessionTabLocal(); } }, - [close, closeTabLocal, privateData, userHandleB64u, globalUserHandleB64u, setCachedUsers], + [closeSessionTabLocal, privateData], ); const openPrivateData = async (): Promise<[PrivateData, CryptoKey]> => { @@ -181,9 +199,6 @@ export function useLocalStorageKeystore(): LocalStorageKeystore { { exportedMainKey, privateData }: UnlockSuccess, user: CachedUser | UserData | null, ): Promise => { - setMainKey(exportedMainKey); - setPrivateData(privateData); - if (user) { const userHandleB64u = ("prfKeys" in user ? user.userHandleB64u @@ -199,13 +214,21 @@ export function useLocalStorageKeystore(): LocalStorageKeystore { ); setUserHandleB64u(userHandleB64u); + + // This must happen before setPrivateData in order to prevent the + // useEffect updating cachedUsers from corrupting cache entries for other + // users logged in in other tabs. setGlobalUserHandleB64u(userHandleB64u); + setCachedUsers((cachedUsers) => { // Move most recently used user to front of list const otherUsers = (cachedUsers || []).filter((cu) => cu.userHandleB64u !== newUser.userHandleB64u); return [newUser, ...otherUsers]; }); } + + setMainKey(exportedMainKey); + setPrivateData(privateData); }; const init = async ( diff --git a/src/services/keystoreEvents.ts b/src/services/keystoreEvents.ts new file mode 100644 index 000000000..45317d02d --- /dev/null +++ b/src/services/keystoreEvents.ts @@ -0,0 +1,3 @@ +// keystoreEvents.js +const keystoreEvents = new EventTarget(); +export default keystoreEvents;