fix(auth): persist session to localStorage for cross-tab support
Session metadata and full session now written to both sessionStorage and localStorage so that new tabs and window.open() inherit the auth state without requiring a fresh login. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -144,12 +144,12 @@ export class AuthSessionStore {
|
|||||||
private readPersistedMetadata(
|
private readPersistedMetadata(
|
||||||
restoredSession: AuthSession | null
|
restoredSession: AuthSession | null
|
||||||
): PersistedSessionMetadata | null {
|
): PersistedSessionMetadata | null {
|
||||||
if (typeof sessionStorage === 'undefined') {
|
if (typeof localStorage === 'undefined') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const raw = sessionStorage.getItem(SESSION_STORAGE_KEY);
|
const raw = localStorage.getItem(SESSION_STORAGE_KEY);
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
if (!restoredSession) {
|
if (!restoredSession) {
|
||||||
return null;
|
return null;
|
||||||
@@ -165,7 +165,7 @@ export class AuthSessionStore {
|
|||||||
typeof parsed.issuedAtEpochMs !== 'number' ||
|
typeof parsed.issuedAtEpochMs !== 'number' ||
|
||||||
typeof parsed.dpopKeyThumbprint !== 'string'
|
typeof parsed.dpopKeyThumbprint !== 'string'
|
||||||
) {
|
) {
|
||||||
sessionStorage.removeItem(SESSION_STORAGE_KEY);
|
localStorage.removeItem(SESSION_STORAGE_KEY);
|
||||||
return restoredSession ? this.toMetadata(restoredSession) : null;
|
return restoredSession ? this.toMetadata(restoredSession) : null;
|
||||||
}
|
}
|
||||||
const tenantId =
|
const tenantId =
|
||||||
@@ -180,37 +180,48 @@ export class AuthSessionStore {
|
|||||||
tenantId,
|
tenantId,
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
sessionStorage.removeItem(SESSION_STORAGE_KEY);
|
localStorage.removeItem(SESSION_STORAGE_KEY);
|
||||||
return restoredSession ? this.toMetadata(restoredSession) : null;
|
return restoredSession ? this.toMetadata(restoredSession) : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private readPersistedSession(): AuthSession | null {
|
private readPersistedSession(): AuthSession | null {
|
||||||
if (typeof sessionStorage === 'undefined') {
|
// Try sessionStorage first (same-tab, survives F5), then fall back
|
||||||
|
// to localStorage (cross-tab, survives new-tab / window.open).
|
||||||
|
const fromSession = this.readSessionFrom('session');
|
||||||
|
if (fromSession) {
|
||||||
|
return fromSession;
|
||||||
|
}
|
||||||
|
return this.readSessionFrom('local');
|
||||||
|
}
|
||||||
|
|
||||||
|
private readSessionFrom(storage: 'session' | 'local'): AuthSession | null {
|
||||||
|
const store = storage === 'session' ? sessionStorage : localStorage;
|
||||||
|
if (typeof store === 'undefined') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const raw = sessionStorage.getItem(FULL_SESSION_STORAGE_KEY);
|
const raw = store.getItem(FULL_SESSION_STORAGE_KEY);
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsed = JSON.parse(raw) as AuthSession;
|
const parsed = JSON.parse(raw) as AuthSession;
|
||||||
if (!this.isValidSession(parsed)) {
|
if (!this.isValidSession(parsed)) {
|
||||||
sessionStorage.removeItem(FULL_SESSION_STORAGE_KEY);
|
store.removeItem(FULL_SESSION_STORAGE_KEY);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsed.tokens.expiresAtEpochMs <= Date.now()) {
|
if (parsed.tokens.expiresAtEpochMs <= Date.now()) {
|
||||||
sessionStorage.removeItem(FULL_SESSION_STORAGE_KEY);
|
store.removeItem(FULL_SESSION_STORAGE_KEY);
|
||||||
this.restoredSessionExpired = true;
|
this.restoredSessionExpired = true;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return parsed;
|
return parsed;
|
||||||
} catch {
|
} catch {
|
||||||
sessionStorage.removeItem(FULL_SESSION_STORAGE_KEY);
|
store.removeItem(FULL_SESSION_STORAGE_KEY);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -262,31 +273,28 @@ export class AuthSessionStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private persistMetadata(metadata: PersistedSessionMetadata): void {
|
private persistMetadata(metadata: PersistedSessionMetadata): void {
|
||||||
if (typeof sessionStorage === 'undefined') {
|
if (typeof localStorage === 'undefined') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sessionStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(metadata));
|
localStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(metadata));
|
||||||
}
|
}
|
||||||
|
|
||||||
private persistSession(session: AuthSession): void {
|
private persistSession(session: AuthSession): void {
|
||||||
if (typeof sessionStorage === 'undefined') {
|
const json = JSON.stringify(session);
|
||||||
return;
|
try { sessionStorage.setItem(FULL_SESSION_STORAGE_KEY, json); } catch { /* SSR / quota */ }
|
||||||
}
|
try { localStorage.setItem(FULL_SESSION_STORAGE_KEY, json); } catch { /* SSR / quota */ }
|
||||||
sessionStorage.setItem(FULL_SESSION_STORAGE_KEY, JSON.stringify(session));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private clearPersistedMetadata(): void {
|
private clearPersistedMetadata(): void {
|
||||||
if (typeof sessionStorage === 'undefined') {
|
if (typeof localStorage === 'undefined') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sessionStorage.removeItem(SESSION_STORAGE_KEY);
|
localStorage.removeItem(SESSION_STORAGE_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
private clearPersistedSession(): void {
|
private clearPersistedSession(): void {
|
||||||
if (typeof sessionStorage === 'undefined') {
|
try { sessionStorage.removeItem(FULL_SESSION_STORAGE_KEY); } catch { /* SSR */ }
|
||||||
return;
|
try { localStorage.removeItem(FULL_SESSION_STORAGE_KEY); } catch { /* SSR */ }
|
||||||
}
|
|
||||||
sessionStorage.removeItem(FULL_SESSION_STORAGE_KEY);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getActiveTenantId(): string | null {
|
getActiveTenantId(): string | null {
|
||||||
|
|||||||
Reference in New Issue
Block a user