fix: persist OIDC token in localStorage instead of sessionStorage
Bug: opening calendar in a new browser tab forced a re-login every time, even when the user was authenticated in another tab. Root cause: tokenSet/userData/redirectState were stored in sessionStorage, which is per-tab by design. New tab → empty sessionStorage → app redirects to OIDC. With Dex's password-DB connector (no IdP-side session cookie), Dex re-prompts for credentials. Mail (tmail-flutter) has the same OIDC flow but persists tokens in IndexedDB (Hive box `tokenoidccache`) which IS shared across tabs — hence mail SSO worked but calendar didn't. Fix: replace sessionStorage with localStorage for the auth-related keys (tokenSet, userData, redirectState). localStorage is shared across tabs of the same origin. Other sessionStorage usages (e.g. eventUpdateModalReopen) are unchanged — they are genuinely per-tab. Files touched: - src/features/User/useInitializeApp.ts (load + setup redirect state) - src/features/User/LoginCallback.tsx (save tokens after callback, read pending redirectState, removeItem on completion) - src/utils/apiUtils.ts (write redirectState during 401-driven SSO redirect) - src/components/Calendar/hooks/useUtilMenus.ts (logout cleanup) Verified: open clean tab → login alice → open another tab on calendar.workavia.local → loaded silently with avatar visible, no login form prompted.
This commit is contained in:
@@ -58,7 +58,7 @@ export const useUtilMenus = (): {
|
||||
} catch (error) {
|
||||
console.error('Logout failed:', error)
|
||||
} finally {
|
||||
sessionStorage.removeItem('tokenSet')
|
||||
localStorage.removeItem('tokenSet')
|
||||
handleUserMenuClose()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,26 +27,26 @@ export function CallbackResume() {
|
||||
|
||||
const runCallback = async () => {
|
||||
// Read redirectState inside useEffect to avoid stale closures
|
||||
const saved = sessionStorage.getItem('redirectState')
|
||||
? JSON.parse(sessionStorage.getItem('redirectState') ?? '{}')
|
||||
const saved = localStorage.getItem('redirectState')
|
||||
? JSON.parse(localStorage.getItem('redirectState') ?? '{}')
|
||||
: null
|
||||
|
||||
// Check if we have saved tokens (already logged in)
|
||||
const savedToken = sessionStorage.getItem('tokenSet')
|
||||
? JSON.parse(sessionStorage.getItem('tokenSet') ?? '{}')
|
||||
const savedToken = localStorage.getItem('tokenSet')
|
||||
? JSON.parse(localStorage.getItem('tokenSet') ?? '{}')
|
||||
: null
|
||||
|
||||
// If no redirectState but we have saved session, just go home
|
||||
// This can happen if user refreshes callback page or gets redirected here after already logged in
|
||||
if (!saved?.code_verifier) {
|
||||
if (savedToken) {
|
||||
sessionStorage.removeItem('redirectState')
|
||||
localStorage.removeItem('redirectState')
|
||||
dispatch(replace('/'))
|
||||
return
|
||||
}
|
||||
|
||||
console.warn('Missing redirectState')
|
||||
sessionStorage.removeItem('redirectState')
|
||||
localStorage.removeItem('redirectState')
|
||||
dispatch(replace('/'))
|
||||
return
|
||||
}
|
||||
@@ -60,10 +60,14 @@ export function CallbackResume() {
|
||||
throw new Error('OAuth callback failed')
|
||||
}
|
||||
|
||||
// IMPORTANT: Save tokens to sessionStorage FIRST before making any API calls
|
||||
// because API calls will read token from sessionStorage
|
||||
sessionStorage.setItem('tokenSet', JSON.stringify(data.tokenSet))
|
||||
sessionStorage.setItem('userData', JSON.stringify(data.userinfo))
|
||||
// IMPORTANT: Save tokens to localStorage FIRST before making any API calls
|
||||
// because API calls will read token from localStorage.
|
||||
// localStorage (not sessionStorage) so the OIDC session is shared
|
||||
// across browser tabs — otherwise each new tab forces a re-login
|
||||
// since Dex (with enablePasswordDB connector) does NOT keep an
|
||||
// IdP-side session cookie.
|
||||
localStorage.setItem('tokenSet', JSON.stringify(data.tokenSet))
|
||||
localStorage.setItem('userData', JSON.stringify(data.userinfo))
|
||||
|
||||
dispatch(setUserData(data.userinfo))
|
||||
dispatch(setTokens(data.tokenSet))
|
||||
@@ -71,7 +75,7 @@ export function CallbackResume() {
|
||||
await dispatch(getOpenPaasUserDataAsync())
|
||||
await dispatch(getCalendarsListAsync())
|
||||
|
||||
sessionStorage.removeItem('redirectState')
|
||||
localStorage.removeItem('redirectState')
|
||||
} catch (e) {
|
||||
console.error('OIDC callback error:', e)
|
||||
dispatch(setAppLoading(false))
|
||||
|
||||
@@ -23,11 +23,11 @@ export function useInitializeApp() {
|
||||
hasInitiatedRef.current = true
|
||||
|
||||
const initiateLogin = async () => {
|
||||
const savedToken = sessionStorage.getItem('tokenSet')
|
||||
? JSON.parse(sessionStorage.getItem('tokenSet') ?? '{}')
|
||||
const savedToken = localStorage.getItem('tokenSet')
|
||||
? JSON.parse(localStorage.getItem('tokenSet') ?? '{}')
|
||||
: null
|
||||
const savedUser = sessionStorage.getItem('userData')
|
||||
? JSON.parse(sessionStorage.getItem('userData') ?? '{}')
|
||||
const savedUser = localStorage.getItem('userData')
|
||||
? JSON.parse(localStorage.getItem('userData') ?? '{}')
|
||||
: null
|
||||
|
||||
if (savedToken && savedUser) {
|
||||
@@ -45,7 +45,7 @@ export function useInitializeApp() {
|
||||
}
|
||||
|
||||
const loginurl = await Auth()
|
||||
sessionStorage.setItem(
|
||||
localStorage.setItem(
|
||||
'redirectState',
|
||||
JSON.stringify({
|
||||
code_verifier: loginurl.code_verifier,
|
||||
|
||||
@@ -35,7 +35,7 @@ const redirectSSO = async (
|
||||
try {
|
||||
const loginurl = await Auth()
|
||||
|
||||
sessionStorage.setItem(
|
||||
localStorage.setItem(
|
||||
'redirectState',
|
||||
JSON.stringify({
|
||||
code_verifier: loginurl.code_verifier,
|
||||
@@ -85,9 +85,9 @@ export const api: KyInstance = ky.extend({
|
||||
hooks: {
|
||||
beforeRequest: [
|
||||
async (request: KyRequest): Promise<KyRequest> => {
|
||||
const saved = sessionStorage.getItem('tokenSet')
|
||||
const saved = localStorage.getItem('tokenSet')
|
||||
? (JSON.parse(
|
||||
sessionStorage.getItem('tokenSet') ?? '{}'
|
||||
localStorage.getItem('tokenSet') ?? '{}'
|
||||
) as TokenEndpointResponse & TokenEndpointResponseHelpers)
|
||||
: null
|
||||
const access_token = saved?.access_token as string
|
||||
|
||||
Reference in New Issue
Block a user