UNPKG

manage-token-sessions

Version:

A flexible token session manager for handling access/refresh token pairs with automatic refresh and cross-domain support

367 lines (291 loc) โ€ข 11.5 kB
# Token Session Manager A flexible token session manager for handling access/refresh token pairs with automatic refresh and cross-domain support. Inspired by Auth0's battle-tested approach to token management. ## Features - ๐Ÿ”„ **Automatic Token Refresh**: Proactively refreshes tokens before expiration - ๐Ÿช **Flexible Storage**: Support for localStorage, sessionStorage, cookies, and custom storage - ๐ŸŒ **Cross-Domain Support**: Cookie storage with subdomain support for multi-app authentication - ๐Ÿ”’ **JWT Decoding**: Extracts expiration times from JWT tokens without verification - ๐Ÿ”— **Cross-Tab Synchronization**: Prevents concurrent refresh attempts across browser tabs - ๐Ÿ‘๏ธ **Tab Focus Detection**: Automatically checks auth state when tab gains focus - ๐ŸŽฃ **Lifecycle Hooks**: Callbacks for session events (started, refreshed, expired, errors) - ๐Ÿงช **Well Tested**: Comprehensive test suite with 100% coverage - ๐Ÿ“ฆ **TypeScript**: Full TypeScript support with detailed type definitions - ๐Ÿชถ **Lightweight**: Minimal dependencies, tree-shakeable ## Installation ```bash npm install manage-token-sessions # or yarn add manage-token-sessions # or pnpm add manage-token-sessions ``` ## Quick Start ```typescript import { TokenSessionManager, LocalStorageAdapter } from 'manage-token-sessions'; // Define your refresh function const refreshTokens = async (refreshToken: string) => { const response = await fetch('/api/auth/refresh', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refreshToken }) }); const data = await response.json(); return { accessToken: data.accessToken, refreshToken: data.refreshToken }; }; // Create the session manager const sessionManager = new TokenSessionManager({ refreshTokenFn: refreshTokens, storage: new LocalStorageAdapter(), onSessionRefreshed: (session) => { console.log('Session refreshed!', session); }, onSessionExpired: () => { console.log('Session expired, redirecting to login...'); window.location.href = '/login'; } }); // Start a session after login await sessionManager.startSession({ accessToken: 'your-jwt-access-token', refreshToken: 'your-refresh-token' }); // Get current access token (automatically refreshes if needed) const accessToken = await sessionManager.getCurrentAccessToken(); // Use the token in API calls fetch('/api/protected', { headers: { 'Authorization': `Bearer ${accessToken}` } }); ``` ## Storage Options ### LocalStorage (Default) ```typescript import { LocalStorageAdapter } from 'manage-token-sessions'; const sessionManager = new TokenSessionManager({ refreshTokenFn: refreshTokens, storage: new LocalStorageAdapter() }); ``` ### SessionStorage ```typescript import { SessionStorageAdapter } from 'manage-token-sessions'; const sessionManager = new TokenSessionManager({ refreshTokenFn: refreshTokens, storage: new SessionStorageAdapter() }); ``` ### Cookies (Cross-Domain Support) ```typescript import { CookieStorageAdapter } from 'manage-token-sessions'; // Basic cookie storage const sessionManager = new TokenSessionManager({ refreshTokenFn: refreshTokens, storage: new CookieStorageAdapter() }); // Cross-subdomain cookie storage const crossDomainSessionManager = new TokenSessionManager({ refreshTokenFn: refreshTokens, storage: new CookieStorageAdapter({ domain: '.example.com', // Works across app1.example.com, app2.example.com, etc. secure: true, sameSite: 'lax' }) }); ``` ### Memory Storage (Testing) ```typescript import { MemoryStorageAdapter } from 'manage-token-sessions'; const sessionManager = new TokenSessionManager({ refreshTokenFn: refreshTokens, storage: new MemoryStorageAdapter() }); ``` ### Custom Storage ```typescript import { TokenStorage } from 'manage-token-sessions'; class CustomStorageAdapter implements TokenStorage { async get(key: string): Promise<string | null> { // Your custom get implementation return null; } async set(key: string, value: string): Promise<void> { // Your custom set implementation } async remove(key: string): Promise<void> { // Your custom remove implementation } } const sessionManager = new TokenSessionManager({ refreshTokenFn: refreshTokens, storage: new CustomStorageAdapter() }); ``` ## Configuration Options ```typescript const sessionManager = new TokenSessionManager({ // Required: Function to refresh tokens refreshTokenFn: async (refreshToken) => ({ accessToken, refreshToken }), // Optional: Storage adapter (default: LocalStorageAdapter) storage: new LocalStorageAdapter(), // Optional: Storage key (default: '@token-sessions@') storageKey: 'my-app-session', // Optional: Refresh buffer in seconds (default: 60) // Tokens are refreshed this many seconds before expiry expiryBufferSeconds: 120, // Optional: Refresh check interval in milliseconds (default: 30000) refreshIntervalMs: 15000, // Optional: Cross-tab lock configuration lockOptions: { timeout: 5000, // Lock timeout in milliseconds retries: 10, // Number of retry attempts retryDelay: 100 // Delay between retries }, // Optional: Tab focus authentication check (default: true) checkOnFocus: true, // Check auth state when tab gains focus focusDebounce: 100, // Debounce focus events in milliseconds // Optional: Lifecycle hooks onSessionStarted: (session) => console.log('Session started', session), onSessionRefreshed: (session) => console.log('Session refreshed', session), onSessionExpired: () => console.log('Session expired'), onSessionError: (error) => console.error('Session error', error), onRefreshError: (error) => console.error('Refresh error', error), onFocusCheck: (session) => console.log('Focus check completed', session) }); ``` ## API Reference ### TokenSessionManager #### Methods - `startSession(tokens, metadata?)`: Start a new session - `getCurrentAccessToken()`: Get current access token (auto-refreshes if needed) - `getCurrentSession()`: Get current session data - `refreshSession()`: Manually refresh the session - `checkAuthState()`: Manually trigger a focus check and return current session - `endSession()`: End the current session - `hasActiveSession()`: Check if there's an active session - `destroy()`: Clean up resources #### Events - `onSessionStarted(session)`: Called when a session is started - `onSessionRefreshed(session)`: Called when tokens are refreshed - `onSessionExpired()`: Called when session expires - `onSessionError(error)`: Called on session errors - `onRefreshError(error)`: Called on refresh errors ## Cross-Tab Synchronization The package automatically prevents concurrent token refresh attempts across multiple browser tabs using a lock mechanism (similar to Auth0's approach). This ensures that: - Only one tab refreshes tokens at a time - Other tabs wait for the refresh to complete - No duplicate refresh requests are made - Token consistency is maintained across all tabs ```typescript // Multiple tabs with the same session manager configuration // will automatically coordinate refresh attempts const sessionManager = new TokenSessionManager({ refreshTokenFn: refreshTokens, storage: new LocalStorageAdapter(), // Lock configuration (optional) lockOptions: { timeout: 5000, // Wait up to 5 seconds for lock retries: 10, // Retry 10 times if lock fails retryDelay: 100 // Wait 100ms between retries } }); // Tab 1: Triggers refresh await sessionManager.refreshSession(); // Tab 2: Waits for Tab 1 to complete, then uses the new token const token = await sessionManager.getCurrentAccessToken(); ``` ## Tab Focus Authentication Check The package automatically checks authentication state when a browser tab gains focus. This provides instant feedback when users switch between tabs and ensures the UI reflects the current session state immediately. ### How it works: - **Automatic Detection**: When you switch to a tab, it immediately validates the current session - **Instant Sync**: No waiting for the periodic refresh interval (default 30 seconds) - **Cross-Tab Login**: If logged in from another tab, switching back shows the authenticated state instantly - **Cross-Tab Logout**: If logged out in another tab, switching back shows the logged-out state instantly - **Session Validation**: Checks if stored tokens are still valid and not expired - **Clock Synchronization**: Aligns refresh timers with tokens refreshed by other tabs ### Configuration: ```typescript const sessionManager = new TokenSessionManager({ refreshTokenFn: refreshTokens, // Tab focus options (all optional) checkOnFocus: true, // Enable focus checking (default: true) focusDebounce: 100, // Debounce rapid focus events (default: 100ms) // Focus check callback onFocusCheck: (session) => { console.log('Tab focused, session state:', session ? 'authenticated' : 'logged out'); } }); // Manual focus check (useful for testing) const currentSession = await sessionManager.checkAuthState(); ``` ### Example Scenarios: ```typescript // Scenario 1: Login in another tab // Tab A: User is logged out // Tab B: User logs in successfully // Tab A: User switches back โ†’ onSessionStarted() fires โ†’ UI updates to authenticated // Scenario 2: Logout in another tab // Tab A: User is logged in // Tab B: User logs out // Tab A: User switches back โ†’ onSessionExpired() fires โ†’ UI updates to logged out // Scenario 3: Token refresh in another tab // Tab A: Token expires in 30 seconds // Tab B: Token gets refreshed โ†’ now expires in 1 hour // Tab A: User switches back โ†’ Timer synchronizes to 1 hour expiry ``` ### Benefits: โœ… **Instant UI Updates**: No delay when switching between tabs โœ… **Better UX**: Immediate feedback on authentication state changes โœ… **Bidirectional Sync**: Detects both login and logout from other tabs โœ… **Security**: Quick detection of session changes from other tabs โœ… **Clock Synchronization**: Aligns with token refreshes from other tabs โœ… **Efficiency**: Only checks when user actually focuses the tab ## Cross-Domain Authentication For applications spanning multiple subdomains, use cookie storage with a shared domain: ```typescript // On auth.example.com (login page) const authSessionManager = new TokenSessionManager({ refreshTokenFn: refreshTokens, storage: new CookieStorageAdapter({ domain: '.example.com', secure: true, sameSite: 'lax' }) }); // After successful login await authSessionManager.startSession(tokens); // On app1.example.com and app2.example.com const appSessionManager = new TokenSessionManager({ refreshTokenFn: refreshTokens, storage: new CookieStorageAdapter({ domain: '.example.com', secure: true, sameSite: 'lax' }) }); // Session is automatically available across subdomains const accessToken = await appSessionManager.getCurrentAccessToken(); ``` ## Error Handling ```typescript const sessionManager = new TokenSessionManager({ refreshTokenFn: refreshTokens, onRefreshError: async (error) => { if (error.message.includes('invalid_grant')) { // Refresh token is invalid, redirect to login window.location.href = '/login'; } else { // Network error, retry later console.error('Refresh failed, will retry:', error); } }, onSessionError: (error) => { console.error('Session error:', error); } }); ``` ## License MIT