From 0261a7de70bff72b777ea5573ddd9eab43fc73a9 Mon Sep 17 00:00:00 2001 From: Swati Tiwari Date: Fri, 31 Oct 2025 13:56:23 +0530 Subject: [PATCH] Add configurable token storage and logout method --- src/core/auth/service.ts | 23 ++++++++++++++++++++ src/core/auth/token-manager.ts | 38 ++++++++++++++++++++-------------- src/core/config/config.ts | 7 ++++++- src/core/config/sdk-config.ts | 10 +++++++++ src/index.ts | 2 +- src/uipath.ts | 12 ++++++++++- 6 files changed, 73 insertions(+), 19 deletions(-) diff --git a/src/core/auth/service.ts b/src/core/auth/service.ts index 62d19095..45eb3858 100644 --- a/src/core/auth/service.ts +++ b/src/core/auth/service.ts @@ -212,6 +212,29 @@ export class AuthService extends BaseService { return this.tokenManager.hasValidToken(); } + /** + * Clears authentication state (logout) + * Removes tokens from storage and clears OAuth context + */ + public logout(): void { + // Clear the token from storage and memory + this.tokenManager.clearToken(); + + // Clear OAuth-specific session storage items + // Note: These items are normally cleared after successful authentication in _handleOAuthCallback(), + // but we clear them here as well to handle edge cases where OAuth flow was interrupted: + // - User navigated away during OAuth flow (before callback completed) + // - OAuth error occurred that didn't clear these items + if (isBrowser) { + try { + sessionStorage.removeItem('uipath_sdk_oauth_context'); + sessionStorage.removeItem('uipath_sdk_code_verifier'); + } catch (error) { + console.warn('Failed to clear OAuth context from session storage', error); + } + } + } + /** * Get the current token */ diff --git a/src/core/auth/token-manager.ts b/src/core/auth/token-manager.ts index c69a3f06..39c3bbcf 100644 --- a/src/core/auth/token-manager.ts +++ b/src/core/auth/token-manager.ts @@ -7,14 +7,15 @@ import { Config } from '../config/config'; /** * TokenManager is responsible for managing authentication tokens. * It provides token operations for a specific client ID. - * - For OAuth tokens: Uses session storage with client ID-based keys + * - For OAuth tokens: Uses configured storage (localStorage or sessionStorage) with client ID-based keys * - For Secret tokens: Stores only in memory, allowing multiple instances */ export class TokenManager { private currentToken?: TokenInfo; private readonly STORAGE_KEY_PREFIX = 'uipath_sdk_user_token-'; private refreshPromise: Promise | null = null; - + private readonly storage?: Storage; + /** * Creates a new TokenManager instance * @param executionContext The execution context @@ -25,7 +26,12 @@ export class TokenManager { private executionContext: ExecutionContext, private config: Config, private isOAuth: boolean = false - ) {} + ) { + // Determine which storage to use based on config (only in browser environments) + if (isBrowser) { + this.storage = config.tokenStorage === 'localStorage' ? localStorage : sessionStorage; + } + } /** * Checks if a token is expired @@ -53,13 +59,13 @@ export class TokenManager { * @returns true if a valid token was loaded, false otherwise */ public loadFromStorage(): boolean { - // Only OAuth tokens are stored in session storage + // Only OAuth tokens are stored in storage if (!isBrowser || !this.isOAuth) { return false; } try { - const storedToken = sessionStorage.getItem(this._getStorageKey()); + const storedToken = this.storage!.getItem(this._getStorageKey()); if (!storedToken) { return false; } @@ -67,14 +73,14 @@ export class TokenManager { const tokenInfo = this._parseTokenInfo(storedToken); if (!tokenInfo) { // Invalid token format, clear it - sessionStorage.removeItem(this._getStorageKey()); + this.storage!.removeItem(this._getStorageKey()); return false; } // Check if token is expired if (this.isTokenExpired(tokenInfo)) { // Token expired, clear it - sessionStorage.removeItem(this._getStorageKey()); + this.storage!.removeItem(this._getStorageKey()); return false; } @@ -83,7 +89,7 @@ export class TokenManager { this._updateExecutionContext(tokenInfo); return true; } catch (error) { - console.warn('Failed to load token from session storage', error); + console.warn('Failed to load token from storage', error); return false; } } @@ -137,13 +143,13 @@ export class TokenManager { // Store token in execution context this._updateExecutionContext(tokenInfo); - - // Store in session storage if in browser and this is an OAuth token + + // Store in configured storage if in browser and this is an OAuth token if (isBrowser && this.isOAuth) { try { - sessionStorage.setItem(this._getStorageKey(), JSON.stringify(tokenInfo)); + this.storage!.setItem(this._getStorageKey(), JSON.stringify(tokenInfo)); } catch (error) { - console.warn('Failed to store token in session storage', error); + console.warn('Failed to store token in storage', error); } } } @@ -186,13 +192,13 @@ export class TokenManager { const headers = this.executionContext.getHeaders(); delete headers['Authorization']; this.executionContext.setHeaders(headers); - - // Remove from session storage if this is an OAuth token + + // Remove from configured storage if this is an OAuth token if (isBrowser && this.isOAuth) { try { - sessionStorage.removeItem(this._getStorageKey()); + this.storage!.removeItem(this._getStorageKey()); } catch (error) { - console.warn('Failed to remove token from session storage', error); + console.warn('Failed to remove token from storage', error); } } } diff --git a/src/core/config/config.ts b/src/core/config/config.ts index 4e33933c..10cf219b 100644 --- a/src/core/config/config.ts +++ b/src/core/config/config.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import { TokenStorage, TOKEN_STORAGE_OPTIONS } from './sdk-config'; export const ConfigSchema = z.object({ baseUrl: z.string().url().default('https://cloud.uipath.com'), @@ -7,7 +8,8 @@ export const ConfigSchema = z.object({ secret: z.string().optional(), clientId: z.string().optional(), redirectUri: z.string().url().optional(), - scope: z.string().optional() + scope: z.string().optional(), + tokenStorage: z.enum(TOKEN_STORAGE_OPTIONS).optional() }); export type Config = z.infer; @@ -20,6 +22,7 @@ interface ConfigOptions { clientId?: string; redirectUri?: string; scope?: string; + tokenStorage?: TokenStorage; } export class UiPathConfig { @@ -30,6 +33,7 @@ export class UiPathConfig { public readonly clientId?: string; public readonly redirectUri?: string; public readonly scope?: string; + public readonly tokenStorage: TokenStorage; // Always defined (defaults to 'sessionStorage') constructor(options: ConfigOptions) { this.baseUrl = options.baseUrl; @@ -39,6 +43,7 @@ export class UiPathConfig { this.clientId = options.clientId; this.redirectUri = options.redirectUri; this.scope = options.scope; + this.tokenStorage = options.tokenStorage || 'sessionStorage'; // Default to sessionStorage } } diff --git a/src/core/config/sdk-config.ts b/src/core/config/sdk-config.ts index b03b3fec..ec350f8c 100644 --- a/src/core/config/sdk-config.ts +++ b/src/core/config/sdk-config.ts @@ -1,8 +1,18 @@ +// Token storage options +export const TOKEN_STORAGE_OPTIONS = ['localStorage', 'sessionStorage'] as const; +export type TokenStorage = typeof TOKEN_STORAGE_OPTIONS[number]; + // Base configuration with common required fields export interface BaseConfig { baseUrl: string; orgName: string; tenantName: string; + /** + * Token storage mechanism (optional, defaults to 'sessionStorage') + * - 'sessionStorage': Tokens isolated per tab, cleared when tab closes (default) + * - 'localStorage': Tokens shared across tabs, persists until cleared + */ + tokenStorage?: TokenStorage; } // OAuth specific fields diff --git a/src/index.ts b/src/index.ts index 128cfd08..8d4a621b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,7 @@ // Export core functionality export { UiPath } from './uipath'; -export type { UiPathSDKConfig, BaseConfig, OAuthFields } from './core/config/sdk-config'; +export type { UiPathSDKConfig, BaseConfig, OAuthFields, TokenStorage } from './core/config/sdk-config'; // Export all models export * from './models/common'; diff --git a/src/uipath.ts b/src/uipath.ts index b740e5e6..4fcdd83b 100644 --- a/src/uipath.ts +++ b/src/uipath.ts @@ -40,7 +40,8 @@ export class UiPath { secret: hasSecretConfig(config) ? config.secret : undefined, clientId: hasOAuthConfig(config) ? config.clientId : undefined, redirectUri: hasOAuthConfig(config) ? config.redirectUri : undefined, - scope: hasOAuthConfig(config) ? config.scope : undefined + scope: hasOAuthConfig(config) ? config.scope : undefined, + tokenStorage: hasOAuthConfig(config) ? config.tokenStorage : undefined }); this.executionContext = new ExecutionContext(); @@ -151,6 +152,15 @@ export class UiPath { return this.authService.getToken(); } + /** + * Logout the user and clear all authentication state + * Removes tokens from storage and clears OAuth context + */ + public logout(): void { + this.authService.logout(); + this.initialized = false; // Reset initialized state + } + private getService(serviceConstructor: ServiceConstructor): T { const serviceName = serviceConstructor.name; if (!this._services.has(serviceName)) {