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) {
|
} catch (error) {
|
||||||
console.error('Logout failed:', error)
|
console.error('Logout failed:', error)
|
||||||
} finally {
|
} finally {
|
||||||
sessionStorage.removeItem('tokenSet')
|
localStorage.removeItem('tokenSet')
|
||||||
handleUserMenuClose()
|
handleUserMenuClose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,26 +27,26 @@ export function CallbackResume() {
|
|||||||
|
|
||||||
const runCallback = async () => {
|
const runCallback = async () => {
|
||||||
// Read redirectState inside useEffect to avoid stale closures
|
// Read redirectState inside useEffect to avoid stale closures
|
||||||
const saved = sessionStorage.getItem('redirectState')
|
const saved = localStorage.getItem('redirectState')
|
||||||
? JSON.parse(sessionStorage.getItem('redirectState') ?? '{}')
|
? JSON.parse(localStorage.getItem('redirectState') ?? '{}')
|
||||||
: null
|
: null
|
||||||
|
|
||||||
// Check if we have saved tokens (already logged in)
|
// Check if we have saved tokens (already logged in)
|
||||||
const savedToken = sessionStorage.getItem('tokenSet')
|
const savedToken = localStorage.getItem('tokenSet')
|
||||||
? JSON.parse(sessionStorage.getItem('tokenSet') ?? '{}')
|
? JSON.parse(localStorage.getItem('tokenSet') ?? '{}')
|
||||||
: null
|
: null
|
||||||
|
|
||||||
// If no redirectState but we have saved session, just go home
|
// 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
|
// This can happen if user refreshes callback page or gets redirected here after already logged in
|
||||||
if (!saved?.code_verifier) {
|
if (!saved?.code_verifier) {
|
||||||
if (savedToken) {
|
if (savedToken) {
|
||||||
sessionStorage.removeItem('redirectState')
|
localStorage.removeItem('redirectState')
|
||||||
dispatch(replace('/'))
|
dispatch(replace('/'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.warn('Missing redirectState')
|
console.warn('Missing redirectState')
|
||||||
sessionStorage.removeItem('redirectState')
|
localStorage.removeItem('redirectState')
|
||||||
dispatch(replace('/'))
|
dispatch(replace('/'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -60,10 +60,14 @@ export function CallbackResume() {
|
|||||||
throw new Error('OAuth callback failed')
|
throw new Error('OAuth callback failed')
|
||||||
}
|
}
|
||||||
|
|
||||||
// IMPORTANT: Save tokens to sessionStorage FIRST before making any API calls
|
// IMPORTANT: Save tokens to localStorage FIRST before making any API calls
|
||||||
// because API calls will read token from sessionStorage
|
// because API calls will read token from localStorage.
|
||||||
sessionStorage.setItem('tokenSet', JSON.stringify(data.tokenSet))
|
// localStorage (not sessionStorage) so the OIDC session is shared
|
||||||
sessionStorage.setItem('userData', JSON.stringify(data.userinfo))
|
// 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(setUserData(data.userinfo))
|
||||||
dispatch(setTokens(data.tokenSet))
|
dispatch(setTokens(data.tokenSet))
|
||||||
@@ -71,7 +75,7 @@ export function CallbackResume() {
|
|||||||
await dispatch(getOpenPaasUserDataAsync())
|
await dispatch(getOpenPaasUserDataAsync())
|
||||||
await dispatch(getCalendarsListAsync())
|
await dispatch(getCalendarsListAsync())
|
||||||
|
|
||||||
sessionStorage.removeItem('redirectState')
|
localStorage.removeItem('redirectState')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('OIDC callback error:', e)
|
console.error('OIDC callback error:', e)
|
||||||
dispatch(setAppLoading(false))
|
dispatch(setAppLoading(false))
|
||||||
|
|||||||
@@ -23,11 +23,11 @@ export function useInitializeApp() {
|
|||||||
hasInitiatedRef.current = true
|
hasInitiatedRef.current = true
|
||||||
|
|
||||||
const initiateLogin = async () => {
|
const initiateLogin = async () => {
|
||||||
const savedToken = sessionStorage.getItem('tokenSet')
|
const savedToken = localStorage.getItem('tokenSet')
|
||||||
? JSON.parse(sessionStorage.getItem('tokenSet') ?? '{}')
|
? JSON.parse(localStorage.getItem('tokenSet') ?? '{}')
|
||||||
: null
|
: null
|
||||||
const savedUser = sessionStorage.getItem('userData')
|
const savedUser = localStorage.getItem('userData')
|
||||||
? JSON.parse(sessionStorage.getItem('userData') ?? '{}')
|
? JSON.parse(localStorage.getItem('userData') ?? '{}')
|
||||||
: null
|
: null
|
||||||
|
|
||||||
if (savedToken && savedUser) {
|
if (savedToken && savedUser) {
|
||||||
@@ -45,7 +45,7 @@ export function useInitializeApp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const loginurl = await Auth()
|
const loginurl = await Auth()
|
||||||
sessionStorage.setItem(
|
localStorage.setItem(
|
||||||
'redirectState',
|
'redirectState',
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
code_verifier: loginurl.code_verifier,
|
code_verifier: loginurl.code_verifier,
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const redirectSSO = async (
|
|||||||
try {
|
try {
|
||||||
const loginurl = await Auth()
|
const loginurl = await Auth()
|
||||||
|
|
||||||
sessionStorage.setItem(
|
localStorage.setItem(
|
||||||
'redirectState',
|
'redirectState',
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
code_verifier: loginurl.code_verifier,
|
code_verifier: loginurl.code_verifier,
|
||||||
@@ -85,9 +85,9 @@ export const api: KyInstance = ky.extend({
|
|||||||
hooks: {
|
hooks: {
|
||||||
beforeRequest: [
|
beforeRequest: [
|
||||||
async (request: KyRequest): Promise<KyRequest> => {
|
async (request: KyRequest): Promise<KyRequest> => {
|
||||||
const saved = sessionStorage.getItem('tokenSet')
|
const saved = localStorage.getItem('tokenSet')
|
||||||
? (JSON.parse(
|
? (JSON.parse(
|
||||||
sessionStorage.getItem('tokenSet') ?? '{}'
|
localStorage.getItem('tokenSet') ?? '{}'
|
||||||
) as TokenEndpointResponse & TokenEndpointResponseHelpers)
|
) as TokenEndpointResponse & TokenEndpointResponseHelpers)
|
||||||
: null
|
: null
|
||||||
const access_token = saved?.access_token as string
|
const access_token = saved?.access_token as string
|
||||||
|
|||||||
Reference in New Issue
Block a user