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
Markdown
# 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