Package Exports
- manage-token-sessions
Readme
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
npm install manage-token-sessions
# or
yarn add manage-token-sessions
# or
pnpm add manage-token-sessionsQuick Start
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)
import { LocalStorageAdapter } from 'manage-token-sessions';
const sessionManager = new TokenSessionManager({
refreshTokenFn: refreshTokens,
storage: new LocalStorageAdapter()
});SessionStorage
import { SessionStorageAdapter } from 'manage-token-sessions';
const sessionManager = new TokenSessionManager({
refreshTokenFn: refreshTokens,
storage: new SessionStorageAdapter()
});Cookies (Cross-Domain Support)
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)
import { MemoryStorageAdapter } from 'manage-token-sessions';
const sessionManager = new TokenSessionManager({
refreshTokenFn: refreshTokens,
storage: new MemoryStorageAdapter()
});Custom Storage
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
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 sessiongetCurrentAccessToken(): Get current access token (auto-refreshes if needed)getCurrentSession(): Get current session datarefreshSession(): Manually refresh the sessioncheckAuthState(): Manually trigger a focus check and return current sessionendSession(): End the current sessionhasActiveSession(): Check if there's an active sessiondestroy(): Clean up resources
Events
onSessionStarted(session): Called when a session is startedonSessionRefreshed(session): Called when tokens are refreshedonSessionExpired(): Called when session expiresonSessionError(error): Called on session errorsonRefreshError(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
// 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:
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:
// 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 expiryBenefits:
โ 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:
// 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
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