UNPKG

@blureffect/oauth2-token-manager

Version:

A scalable OAuth2 token management library with multi-system support

788 lines (624 loc) โ€ข 22.9 kB
# OAuth2 Token Manager A powerful, storage-agnostic OAuth2 token management library built for scalable multi-system architectures. This library provides comprehensive token lifecycle management with pluggable storage adapters, built-in security features, and support for multiple OAuth2 providers. ## ๐Ÿš€ Features - **๐Ÿ”Œ Storage Agnostic**: Use any storage backend (In-Memory, PostgreSQL, or build your own adapter) - **๐Ÿข Multi-System Support**: Manage tokens across multiple applications/systems - **๐Ÿ” Advanced Security**: PKCE support, state validation, token encryption - **โšก High Performance**: Efficient token validation, caching, and refresh strategies - **๐Ÿ”„ Auto-Refresh**: Automatic token refresh with configurable buffers - **๐Ÿ‘ค User Management**: Comprehensive user lifecycle with email/external ID support - **๐Ÿ“ง Profile Integration**: Automatic profile fetching from OAuth providers - **๐ŸŽฏ Flexible Scoping**: Fine-grained permission management - **๐Ÿ’ก Developer Friendly**: Both context-managed and granular APIs - **๐Ÿงช Fully Tested**: Comprehensive test coverage with Vitest ## ๐Ÿ“ฆ Installation ```bash npm install @blureffect/oauth2-token-manager ``` ### Storage Adapters ```bash # PostgreSQL adapter npm install @blureffect/oauth2-storage-postgres ``` ## ๐Ÿš€ Quick Start ### Simple Setup ```typescript import { OAuth2Client } from '@blureffect/oauth2-token-manager'; // Quick setup for common use cases const oauth = await OAuth2Client.quickSetup('MyApp', { google: { clientId: 'your-google-client-id', clientSecret: 'your-google-client-secret', authorizationUrl: 'https://accounts.google.com/o/oauth2/auth', tokenUrl: 'https://oauth2.googleapis.com/token', redirectUri: 'http://localhost:3000/auth/callback', scopes: ['profile', 'email'], }, github: { clientId: 'your-github-client-id', clientSecret: 'your-github-client-secret', authorizationUrl: 'https://github.com/login/oauth/authorize', tokenUrl: 'https://github.com/login/oauth/access_token', redirectUri: 'http://localhost:3000/auth/callback', scopes: ['user:email'], }, }); // Create or get a user const user = await oauth.getOrCreateUser({ email: 'user@example.com', metadata: { role: 'user' }, }); // Start OAuth flow const { url, state } = await oauth.authorize({ provider: 'google', scopes: ['profile', 'email'], }); // Handle callback const result = await oauth.handleCallback(code, state); console.log('User authenticated:', result.userId); ``` ### Advanced Setup with Custom Storage ```typescript import { OAuth2Client } from '@blureffect/oauth2-token-manager'; import { PostgresStorageFactory } from '@blureffect/oauth2-storage-postgres'; // Custom storage adapter const storage = await PostgresStorageFactory.create({ host: 'localhost', port: 5432, username: 'oauth2_user', password: 'secure_password', database: 'oauth2_db', }); const oauth = new OAuth2Client({ storage, providers: { google: { /* config */ }, github: { /* config */ }, }, }); // Create system and scopes const system = await oauth.createSystem('MyApp'); const scope = await oauth.createScope('api-access', { type: 'access', permissions: ['read:profile', 'write:data'], isolated: true, }); ``` ## ๐Ÿ—๏ธ Architecture ### Core Components ``` โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ OAuth2Client โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Context API โ”‚ โ”‚ Granular API โ”‚ โ”‚ โ”‚ โ”‚ (Simplified) โ”‚ โ”‚ (Full Control) โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Providers โ”‚ โ”‚ Storage โ”‚ โ”‚ Profile โ”‚ โ”‚ (OAuth2) โ”‚ โ”‚ Adapter โ”‚ โ”‚ Fetchers โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` ### Data Model & Token Hierarchy **Important**: Users can have multiple tokens for the same provider within the same scope. This allows for scenarios like different email accounts or token refresh cycles. ```typescript // Systems: Top-level applications/services interface System { id: string; name: string; description?: string; scopes: Scope[]; metadata?: Record<string, any>; } // Scopes: Permission boundaries within systems interface Scope { id: string; systemId: string; name: string; type: 'authentication' | 'access' | 'custom'; permissions: string[]; isolated: boolean; // Whether tokens are isolated to this scope } // Users: Identity within a system interface User { id: string; systemId: string; metadata?: Record<string, any>; } // User Tokens: OAuth2 tokens tied to user/system/scope/provider // A user can have MULTIPLE tokens for the same provider/scope combination interface UserToken { id: string; userId: string; systemId: string; scopeId: string; provider: string; token: OAuth2Token; } ``` ### Token Hierarchy Rules 1. **One User** belongs to **One System** 2. **One User** can have tokens in **Multiple Scopes** within their system 3. **One User** in **One Scope** can have tokens from **Multiple Providers** 4. **One User** in **One Scope** from **One Provider** can have **Multiple Tokens** 5. **Email Uniqueness**: For the same provider, a user cannot have multiple tokens with the same email (validated via profile fetcher) 6. **Cross-Provider Emails**: The same email can exist across different providers ## ๐Ÿ“š API Reference ### OAuth2Client The main client class providing both context-managed and granular APIs. #### Context-Managed API (Recommended) ```typescript // System management await oauth.createSystem('MyApp'); await oauth.useSystem(systemId); // User management const user = await oauth.getOrCreateUser({ email: 'user@example.com' }); await oauth.useUser(userId); // Authorization flow const { url, state } = await oauth.authorize({ provider: 'google' }); const result = await oauth.handleCallback(code, state); // Token operations (uses current context + default scope) // Note: When multiple tokens exist, these methods use the first (most recent) token const accessToken = await oauth.getAccessToken('google'); const validToken = await oauth.ensureValidToken('google'); // Get all user tokens with auto-refresh (for current user) const userTokens = await oauth.getUserTokens(); // Get all valid tokens for a specific user const allTokens = await oauth.getAllValidTokensForUser(userId); // Returns: { provider: string; scopeId: string; token: OAuth2Token; userToken: UserToken }[] // Revoke tokens (uses current context) await oauth.revokeTokens('google'); // Revokes for current user/scope/provider ``` #### Granular API (Advanced) The granular API provides full control over the token hierarchy: ```typescript // === User-Centric Token Queries (Primary Key: User) === // Get ALL tokens for a user across all scopes/providers const userTokens = await oauth.granular.getTokensByUser(userId); // Get tokens for user in specific scope (across all providers) const scopeTokens = await oauth.granular.getTokensByUserAndScope(userId, scopeId); // Get tokens for user with specific provider (across all scopes) const providerTokens = await oauth.granular.getTokensByUserAndProvider(userId, 'google'); // Get tokens for user/scope/provider combination (can be multiple!) const specificTokens = await oauth.granular.getTokensByUserScopeProvider(userId, scopeId, 'google'); // === Cross-User Queries (System/Scope Level) === // Get all tokens in a scope across all users const scopeAllTokens = await oauth.granular.getTokensByScope(systemId, scopeId); // Get all tokens for a provider across all users in system const providerAllTokens = await oauth.granular.getTokensByProvider(systemId, 'google'); // Get all tokens in a system const systemTokens = await oauth.granular.getTokensBySystem(systemId); // === Email-Based Queries === // Find tokens by email (cross-user, cross-provider) const emailTokens = await oauth.granular.findTokensByEmail('user@example.com', systemId); // Find tokens by email in specific scope const emailScopeTokens = await oauth.granular.findTokensByEmailAndScope( 'user@example.com', systemId, scopeId, ); // Find tokens by email for specific provider const emailProviderTokens = await oauth.granular.findTokensByEmailAndProvider( 'user@example.com', systemId, 'google', ); // Find specific token by email/scope/provider (returns single token or null) const specificToken = await oauth.granular.findTokenByEmailScopeProvider( 'user@example.com', systemId, scopeId, 'google', ); // === Token Operations === // Get valid token for user (auto-refresh, takes first if multiple exist) const validToken = await oauth.granular.getValidTokenForUser(userId, scopeId, 'google'); // Get access token for user (convenience method) const accessToken = await oauth.granular.getAccessTokenForUser(userId, scopeId, 'google'); // Save new token for user const savedToken = await oauth.granular.saveTokenForUser( userId, systemId, scopeId, 'google', 'user@example.com', oauthToken, ); // === Token Management === // Delete tokens by different criteria await oauth.granular.deleteTokensByUser(userId); // All tokens for user await oauth.granular.deleteTokensByUserAndScope(userId, scopeId); // User's tokens in scope await oauth.granular.deleteTokensByUserAndProvider(userId, 'google'); // User's tokens for provider ``` ### Storage Adapters #### Built-in Memory Adapter ```typescript import { InMemoryStorageAdapter } from '@blureffect/oauth2-token-manager'; const storage = new InMemoryStorageAdapter(); const oauth = new OAuth2Client({ storage }); ``` #### PostgreSQL Adapter ```typescript import { PostgresStorageFactory } from '@blureffect/oauth2-storage-postgres'; const storage = await PostgresStorageFactory.create({ host: process.env.DB_HOST, port: parseInt(process.env.DB_PORT || '5432'), username: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, ssl: process.env.NODE_ENV === 'production', }); ``` #### Custom Storage Adapter ```typescript import { StorageAdapter } from '@blureffect/oauth2-token-manager'; class MyCustomAdapter implements StorageAdapter { async createSystem(system) { /* implement */ } async getSystem(id) { /* implement */ } // ... implement all required methods } ``` ### Provider Configuration #### Google OAuth2 ```typescript { google: { clientId: 'your-client-id', clientSecret: 'your-client-secret', authorizationUrl: 'https://accounts.google.com/o/oauth2/auth', tokenUrl: 'https://oauth2.googleapis.com/token', redirectUri: 'http://localhost:3000/auth/callback', scopes: ['profile', 'email'], profileUrl: 'https://www.googleapis.com/oauth2/v2/userinfo', usePKCE: true, // Recommended for security } } ``` #### GitHub OAuth2 ```typescript { github: { clientId: 'your-client-id', clientSecret: 'your-client-secret', authorizationUrl: 'https://github.com/login/oauth/authorize', tokenUrl: 'https://github.com/login/oauth/access_token', redirectUri: 'http://localhost:3000/auth/callback', scopes: ['user:email'], profileUrl: 'https://api.github.com/user', } } ``` #### Generic Provider ```typescript { custom: { clientId: 'your-client-id', clientSecret: 'your-client-secret', authorizationUrl: 'https://provider.com/oauth/authorize', tokenUrl: 'https://provider.com/oauth/token', redirectUri: 'http://localhost:3000/auth/callback', scopes: ['read', 'write'], profileUrl: 'https://provider.com/api/user', additionalParams: { audience: 'api.example.com' }, responseRootKey: 'data' // For nested responses } } ``` #### Google OAuth2 with Offline Access ```typescript const oauth = new OAuth2Client({ providers: { google: { clientId: 'your-client-id', clientSecret: 'your-client-secret', redirectUri: 'http://localhost:3000/auth/callback', scopes: ['profile', 'email'], // Override default offline access parameters extraAuthParams: { access_type: 'offline', // Request refresh token prompt: 'consent', // Force consent screen include_granted_scopes: 'true', // Include previously granted scopes }, }, }, }); // The library automatically handles refresh tokens const token = await oauth.getAccessToken('google', { autoRefresh: true, refreshBuffer: 5, // Refresh 5 minutes before expiry }); ``` #### Customizing Authorization Parameters Each provider supports customization through `extraAuthParams` and `additionalParams`: ```typescript { google: { // ... other config ... extraAuthParams: { access_type: 'offline', // For refresh tokens prompt: 'select_account', // Force account selection hd: 'yourdomain.com' // Limit to specific Google Workspace domain } }, microsoft: { // ... other config ... extraAuthParams: { prompt: 'select_account', domain_hint: 'yourdomain.com' } } } ``` Available parameters for Google OAuth2: - `access_type`: 'online' (default) or 'offline' (for refresh tokens) - `prompt`: 'none', 'consent', 'select_account' - `include_granted_scopes`: 'true' or 'false' - `login_hint`: User's email address - `hd`: Google Workspace domain restriction ## ๐Ÿ”ง Advanced Features ### Token Auto-Refresh ```typescript const accessToken = await oauth.getAccessToken('google', { autoRefresh: true, refreshBuffer: 5, // Refresh 5 minutes before expiry expirationBuffer: 30, // Consider expired 30 seconds early }); ``` ### Profile-Based Token Management ```typescript const result = await oauth.handleCallback(code, state, { profileOptions: { checkProfileEmail: true, // Fetch and check email conflicts replaceConflictingTokens: true, // Replace existing tokens with same email mergeUserData: true, // Merge profile data into user metadata }, }); ``` ### Email-Based Operations ```typescript // Get all valid tokens for an email across all providers in a system // Note: This returns an array since one email can have tokens from multiple providers const emailTokens = await oauth.getAllValidTokensForEmail('user@example.com', systemId); // Returns: { provider: string; scopeId: string; token: OAuth2Token; userToken: UserToken }[] // Get specific token by email (returns single token or null) // This enforces the email uniqueness rule within provider/scope const token = await oauth.getTokenForEmail('user@example.com', systemId, scopeId, 'google'); // Get valid token for email with auto-refresh const validToken = await oauth.getValidTokenForEmail( 'user@example.com', systemId, scopeId, 'google', { autoRefresh: true }, ); // Get access token for email const accessToken = await oauth.getAccessTokenForEmail( 'user@example.com', systemId, scopeId, 'google', ); // Execute with valid token for email await oauth.withValidTokenForEmail( 'user@example.com', systemId, scopeId, 'google', async (accessToken) => { console.log('Using token for email:', accessToken); }, ); // Check if email has token for specific provider/scope const hasToken = await oauth.hasTokenForEmail('user@example.com', systemId, scopeId, 'google'); // Revoke tokens for email await oauth.revokeTokensForEmail('user@example.com', systemId, scopeId, 'google'); ``` ### User-Centric Operations (Stateless) For backend APIs where you have explicit user IDs: ```typescript // Get access token for specific user/scope/provider // Note: Takes the first (most recent) token if multiple exist const accessToken = await oauth.getAccessTokenForUser(userId, systemId, scopeId, 'google', { autoRefresh: true, }); // Execute with valid token for specific user await oauth.withValidTokenForUser(userId, systemId, scopeId, 'google', async (accessToken) => { // Make API calls with the token return apiResponse; }); // Get all valid tokens for a user with auto-refresh const userTokens = await oauth.getAllValidTokensForUser(userId, { autoRefresh: true, refreshBuffer: 5, // Refresh 5 minutes before expiry }); // Check if user has tokens for specific provider/scope const hasToken = await oauth.hasTokenForUser(userId, systemId, scopeId, 'google'); // Get user token entity (includes metadata) const userToken = await oauth.getUserTokenForUser(userId, systemId, scopeId, 'google'); // Revoke specific tokens await oauth.revokeTokensForUser(userId, systemId, scopeId, 'google'); ``` ### PKCE Support ```typescript // Enable PKCE for enhanced security const { url, state } = await oauth.authorize({ provider: 'google', usePKCE: true, // Enables PKCE flow }); ``` ### Token Validation ```typescript // Check if token is expired const isExpired = oauth.isTokenExpired(token, { expirationBuffer: 60, // Consider expired 60 seconds early }); // Ensure valid token (auto-refresh if needed) const validToken = await oauth.ensureValidToken('google'); ``` ## ๐Ÿ”’ Security Features ### State Management - Cryptographically secure state generation - Automatic state validation and cleanup - Configurable state expiration ### PKCE (Proof Key for Code Exchange) - Built-in PKCE support for public clients - Automatic code verifier generation - Enhanced security for mobile and SPA applications ### Token Encryption - Secure token storage with optional encryption - Configurable seal keys for sensitive data - Protection against token theft ### Email Validation - Automatic email conflict detection - Profile-based user validation - Cross-provider email consistency ## ๐Ÿงช Testing The library includes comprehensive tests using Vitest: ```bash # Run tests npm test # Run tests with UI npm run test:ui # Run tests with coverage npm run test:coverage # Watch mode npm run test:watch ``` ## ๐Ÿข Multi-System Examples ### SaaS Platform with Multiple Apps ```typescript // Create systems for different applications const crmSystem = await oauth.createSystem('CRM App'); const analyticsSystem = await oauth.createSystem('Analytics Dashboard'); // Create scopes for different access levels await oauth.useSystem(crmSystem.id); const readScope = await oauth.createScope('read-only', { type: 'access', permissions: ['read:contacts', 'read:deals'], isolated: true, }); const adminScope = await oauth.createScope('admin', { type: 'access', permissions: ['*'], isolated: true, }); // Users can have different permissions per system const user = await oauth.getOrCreateUser({ email: 'user@company.com' }); // Authorize for specific system/scope const { url } = await oauth.authorize({ provider: 'google', scopes: ['profile', 'email'], }); ``` ### Multi-Tenant Application ```typescript // Each tenant gets their own system const tenantSystem = await oauth.createSystem(`Tenant-${tenantId}`); // Tenant-specific user management await oauth.useSystem(tenantSystem.id); const tenantUser = await oauth.getOrCreateUser({ email: userEmail, metadata: { tenantId, role: 'admin' }, }); // Tenant-isolated tokens const tokens = await oauth.granular.getTokensBySystem(tenantSystem.id); ``` ## ๐Ÿš€ Production Deployment ### Environment Configuration ```typescript const oauth = new OAuth2Client({ storage: await PostgresStorageFactory.create({ host: process.env.DB_HOST, port: parseInt(process.env.DB_PORT || '5432'), username: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, ssl: { rejectUnauthorized: process.env.NODE_ENV === 'production', }, poolSize: 20, logging: process.env.NODE_ENV === 'development', }), sealKey: process.env.OAUTH2_SEAL_KEY, // For token encryption providers: { google: { clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, redirectUri: process.env.GOOGLE_REDIRECT_URI, // ... other config }, }, }); ``` ### Performance Optimization ```typescript // Use token caching for high-traffic scenarios const accessToken = await oauth.getAccessToken('google', { autoRefresh: true, refreshBuffer: 10, // Refresh early to avoid expiry }); // Batch operations for efficiency const allTokens = await oauth.getAllValidTokensForUser(userId); // Clean up expired states regularly setInterval( async () => { await oauth.cleanup(10 * 60 * 1000); // 10 minutes }, 5 * 60 * 1000, ); // Every 5 minutes ``` ### Error Handling ```typescript try { const token = await oauth.getAccessToken('google'); } catch (error) { if (error.message.includes('Token expired')) { // Handle token expiry const { url } = await oauth.authorize({ provider: 'google' }); // Redirect to re-authorization } else if (error.message.includes('Provider not found')) { // Handle missing provider } } ``` ## ๐Ÿค Contributing 1. Fork the repository 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 3. Commit your changes (`git commit -m 'Add some amazing feature'`) 4. Push to the branch (`git push origin feature/amazing-feature`) 5. Open a Pull Request ### Development Setup ```bash # Install dependencies npm install # Run in development mode npm run dev # Run tests npm test # Build the project npm run build # Lint and format npm run lint:fix npm run format ``` ## ๐Ÿ“„ License This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. ## ๐Ÿ™‹โ€โ™‚๏ธ Support - ๐Ÿ“š [Documentation](https://github.com/blureffect/oauth2-token-manager#readme) - ๐Ÿ› [Issue Tracker](https://github.com/blureffect/oauth2-token-manager/issues) - ๐Ÿ’ฌ [Discussions](https://github.com/blureffect/oauth2-token-manager/discussions) ## ๐Ÿ† Credits Created with โค๏ธ by [Blureffect](https://blureffect.co)