Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions apps/shell-ui/src/auth/ApiKeyAuthProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
//
// Simple auth provider for open-source / local server mode.
// User enters an API key (or leaves blank for open access), which is stored
// in sessionStorage and passed to ConnectionManager.connect().
// in localStorage and passed to ConnectionManager.connect().
//
// Implements the same IAuthProvider interface as CloudAuthProvider so the
// ConnectionManager can use either interchangeably.
Expand Down Expand Up @@ -63,7 +63,7 @@ export class ApiKeyAuthProvider implements IAuthProvider {
/**
* Sign in with an API key.
*
* Stores the key in sessionStorage for the current session. An empty string
* Stores the key in localStorage. An empty string
* is valid (some OSS servers allow unauthenticated access).
*
* @param apiKey - The API key to store.
Expand All @@ -84,7 +84,7 @@ export class ApiKeyAuthProvider implements IAuthProvider {
*/
private async storeToken(token: string): Promise<void> {
try {
sessionStorage.setItem(LS_TOKEN, token);
localStorage.setItem(LS_TOKEN, token);
} catch (e) {
console.error('[ApiKeyAuthProvider] Failed to store token:', e);
}
Expand All @@ -97,7 +97,7 @@ export class ApiKeyAuthProvider implements IAuthProvider {
*/
public async getToken(): Promise<string | null> {
try {
const token = sessionStorage.getItem(LS_TOKEN);
const token = localStorage.getItem(LS_TOKEN);
// For API key mode, empty string is valid (open access)
return token;
} catch {
Expand All @@ -111,7 +111,7 @@ export class ApiKeyAuthProvider implements IAuthProvider {
*/
public async isSignedIn(): Promise<boolean> {
try {
return sessionStorage.getItem(LS_TOKEN) !== null;
return localStorage.getItem(LS_TOKEN) !== null;
} catch {
return false;
}
Expand All @@ -126,7 +126,7 @@ export class ApiKeyAuthProvider implements IAuthProvider {
*/
public async signOut(): Promise<void> {
try {
sessionStorage.removeItem(LS_TOKEN);
localStorage.removeItem(LS_TOKEN);
} catch (e) {
console.error('[ApiKeyAuthProvider] Failed to clear token:', e);
}
Expand Down
10 changes: 5 additions & 5 deletions apps/shell-ui/src/auth/CloudAuthProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
// 2. Full-page redirect to Zitadel authorize endpoint
// 3. Browser redirects back with ?code= parameter
// 4. handleCallback() exchanges code for token via the ConnectionManager
// 5. Token stored in sessionStorage (browser equivalent of SecretStorage)
// 5. Token stored in localStorage (browser equivalent of SecretStorage)
//
// The stored token is picked up by ConnectionManager.connect() — auth is
// decoupled from connection, same as VSCode.
Expand Down Expand Up @@ -173,7 +173,7 @@ export class CloudAuthProvider implements IAuthProvider {
}

// =========================================================================
// TOKEN STORAGE (sessionStorage — browser equivalent of SecretStorage)
// TOKEN STORAGE (localStorage — browser equivalent of SecretStorage)
// =========================================================================

/**
Expand All @@ -183,7 +183,7 @@ export class CloudAuthProvider implements IAuthProvider {
*/
public async storeToken(token: string): Promise<void> {
try {
sessionStorage.setItem(LS_TOKEN, token);
localStorage.setItem(LS_TOKEN, token);
} catch (e) {
console.error('[CloudAuthProvider] Failed to store token:', e);
}
Expand All @@ -196,7 +196,7 @@ export class CloudAuthProvider implements IAuthProvider {
*/
public async getToken(): Promise<string | null> {
try {
const token = sessionStorage.getItem(LS_TOKEN);
const token = localStorage.getItem(LS_TOKEN);
return token || null;
} catch {
return null;
Expand All @@ -220,7 +220,7 @@ export class CloudAuthProvider implements IAuthProvider {
*/
public async signOut(): Promise<void> {
try {
sessionStorage.removeItem(LS_TOKEN);
localStorage.removeItem(LS_TOKEN);
} catch (e) {
console.error('[CloudAuthProvider] Failed to clear token:', e);
}
Expand Down
48 changes: 43 additions & 5 deletions apps/shell-ui/src/connection/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import type { ConnectionMode } from 'shared';
import { BaseManager } from './base-manager';
import { RemoteManager } from './remote-manager';
import { ConnectionFailure, withTimeout } from './errors';
import { shouldReloadForTokenStorageUpdate } from './tokenStorageUpdate';
import { getStoredVerifier, clearStoredVerifier } from '../util/pkce';
import {
LS_TOKEN,
Expand Down Expand Up @@ -315,6 +316,33 @@ export class ConnectionManager implements IConnectionManager {
// click would hit the guard and do nothing. Release it on bfcache restore
// so the button works again without a manual page refresh.
if (typeof window !== 'undefined') {
window.addEventListener('storage', (event) => {
if (event.key !== LS_TOKEN) return;

if (event.newValue === null) {
this.accountInfo = undefined;
this.pendingEvents.clear();
this.clearServicesCache();
this.updateConnectionStatus({
state: ConnectionState.DISCONNECTED,
hasCredentials: false,
lastError: undefined,
progressMessage: undefined,
});
window.location.reload();
return;
}

if (shouldReloadForTokenStorageUpdate({
oldValue: event.oldValue,
newValue: event.newValue,
currentUserToken: this.accountInfo?.userToken,
hasAccountInfo: Boolean(this.accountInfo),
})) {
window.location.reload();
}
});

Comment thread
kgarg2468 marked this conversation as resolved.
window.addEventListener('pageshow', (e) => {
if ((e as PageTransitionEvent).persisted) {
this.oauthStarted = false;
Expand Down Expand Up @@ -839,21 +867,31 @@ export class ConnectionManager implements IConnectionManager {
// TOKEN STORAGE
// =========================================================================

/** Persist a user token to sessionStorage. */
/** Persist a user token to localStorage. */
public saveToken(token: string): void {
try { sessionStorage.setItem(LS_TOKEN, token); } catch (e) {
try { localStorage.setItem(LS_TOKEN, token); } catch (e) {
console.error('[ConnectionManager] Failed to save token:', e);
}
}

/** Load token from sessionStorage. Returns empty string if unavailable. */
/** Load token from localStorage. Migrates the old sessionStorage value once. */
public loadToken(): string {
try { return sessionStorage.getItem(LS_TOKEN) ?? ''; } catch { return ''; }
try {
const token = localStorage.getItem(LS_TOKEN);
if (token !== null) return token;

const sessionToken = sessionStorage.getItem(LS_TOKEN);
if (sessionToken === null) return '';

localStorage.setItem(LS_TOKEN, sessionToken);
sessionStorage.removeItem(LS_TOKEN);
return sessionToken;
} catch { return ''; }
}

/** Clear the persisted token. */
public clearToken(): void {
try { sessionStorage.removeItem(LS_TOKEN); } catch (e) {
try { localStorage.removeItem(LS_TOKEN); } catch (e) {
console.error('[ConnectionManager] Failed to clear token:', e);
}
}
Expand Down
18 changes: 18 additions & 0 deletions apps/shell-ui/src/connection/tokenStorageUpdate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export interface TokenStorageUpdate {
oldValue: string | null;
newValue: string | null;
currentUserToken?: string;
hasAccountInfo: boolean;
}

export function shouldReloadForTokenStorageUpdate(update: TokenStorageUpdate): boolean {
const { oldValue, newValue, currentUserToken, hasAccountInfo } = update;

if (newValue === null) return false;

if (oldValue !== null) {
return currentUserToken !== newValue;
}

return !hasAccountInfo;
}
2 changes: 1 addition & 1 deletion apps/shell-ui/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
// STORAGE KEYS
// =============================================================================

/** sessionStorage: auth token — survives refresh, cleared on tab close. */
/** localStorage: auth token — persists across tabs, windows, and browser restarts. */
export const LS_TOKEN = 'rr:user_token';

/**
Expand Down
Loading