UNPKG

@dainprotocol/oauth2-token-manager

Version:

A scalable OAuth2 token management library with multi-system support

498 lines (388 loc) 12.4 kB
# OAuth2 Token Manager A simple OAuth2 token management library for Node.js. Store and manage OAuth2 tokens with automatic refresh, multiple providers, and pluggable storage. ## 📋 Table of Contents - [Features](#-features) - [Installation](#-installation) - [Quick Start](#-quick-start) - [Core Concepts](#-core-concepts) - [API Reference](#-api-reference) - [Storage Adapters](#-storage-adapters) - [Examples](#-examples) ## 🚀 Features - **Simple Storage**: Tokens stored by provider + email (unique constraint) - **Auto Refresh**: Tokens refresh automatically when expired - **Multiple Providers**: Google, GitHub, Microsoft, Facebook, and custom providers - **Profile Fetching**: Get user profiles during OAuth callback - **Custom Profile Fetchers**: Register custom profile fetchers per storage instance - **Pluggable Storage**: In-memory, PostgreSQL, Drizzle, or custom adapters - **Type Safe**: Full TypeScript support ## 📦 Installation ```bash npm install @dainprotocol/oauth2-token-manager ``` ### Storage Adapters ```bash # PostgreSQL adapter (TypeORM based) npm install @dainprotocol/oauth2-storage-postgres # Drizzle adapter (supports PostgreSQL, MySQL, SQLite) npm install @dainprotocol/oauth2-storage-drizzle ``` ## 🚀 Quick Start ```typescript import { OAuth2Client } from '@dainprotocol/oauth2-token-manager'; // Initialize with provider configurations const oauth = new OAuth2Client({ providers: { google: { clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth', tokenUrl: 'https://oauth2.googleapis.com/token', redirectUri: 'http://localhost:3000/auth/google/callback', scopes: ['profile', 'email'], profileUrl: 'https://www.googleapis.com/oauth2/v2/userinfo', }, github: { clientId: process.env.GITHUB_CLIENT_ID, clientSecret: process.env.GITHUB_CLIENT_SECRET, authorizationUrl: 'https://github.com/login/oauth/authorize', tokenUrl: 'https://github.com/login/oauth/access_token', redirectUri: 'http://localhost:3000/auth/github/callback', scopes: ['user:email'], profileUrl: 'https://api.github.com/user', }, }, }); // Start OAuth flow const { url, state } = await oauth.authorize({ provider: 'google', userId: 'user123', email: 'user@example.com', scopes: ['profile', 'email'], }); // Redirect user to authorization URL res.redirect(url); // Handle OAuth callback const { token, profile } = await oauth.handleCallback(code, state); // Use the token const accessToken = await oauth.getAccessToken('google', 'user@example.com'); ``` ## 🔑 Core Concepts ### How It Works 1. **One token per provider/email**: Each email can have one token per OAuth provider 2. **Automatic override**: Saving a token with same provider + email replaces the old one 3. **Auto refresh**: Expired tokens refresh automatically when you request them 4. **Simple storage**: Just provider (string), userId (string), and email (string) ## 📚 API Reference ### OAuth2Client The main class for managing OAuth2 tokens. ```typescript const oauth = new OAuth2Client({ storage?: StorageAdapter, // Optional, defaults to in-memory providers?: { // OAuth provider configurations [name: string]: OAuth2Config } }); ``` ### Methods #### 🔐 OAuth Flow ##### `authorize(options)` Start OAuth2 authorization flow. ```typescript const { url, state } = await oauth.authorize({ provider: 'google', userId: 'user123', email: 'user@example.com', scopes?: ['profile', 'email'], // Optional usePKCE?: true, // Optional metadata?: { source: 'signup' } // Optional }); // Redirect user to `url` ``` ##### `handleCallback(code, state)` Handle OAuth2 callback and save tokens. ```typescript const { token, profile } = await oauth.handleCallback(code, state); // token: { id, provider, userId, email, token, metadata, ... } // profile: { id, email, name, picture, raw } or undefined ``` #### 🔑 Token Management ##### `getAccessToken(provider, email, options?)` Get access token string (auto-refreshes if expired). ```typescript const accessToken = await oauth.getAccessToken('google', 'user@example.com'); // Returns: "ya29.a0AfH6SMBx..." ``` ##### `getValidToken(provider, email, options?)` Get full token object (auto-refreshes if expired). ```typescript const token = await oauth.getValidToken('google', 'user@example.com'); // Returns: { accessToken, refreshToken, expiresAt, tokenType, scope, ... } ``` #### 🔍 Token Queries ##### `getTokensByUserId(userId)` Get all tokens for a user. ```typescript const tokens = await oauth.getTokensByUserId('user123'); // Returns: StoredToken[] ``` ##### `getTokensByEmail(email)` Get all tokens for an email. ```typescript const tokens = await oauth.getTokensByEmail('user@example.com'); // Returns: StoredToken[] ``` #### 🗑️ Token Cleanup ##### `deleteToken(provider, email)` Delete a specific token. ```typescript const deleted = await oauth.deleteToken('google', 'user@example.com'); // Returns: boolean ``` ##### `cleanupExpiredTokens()` Delete all expired tokens. ```typescript const count = await oauth.cleanupExpiredTokens(); // Returns: number of deleted tokens ``` ##### `cleanupExpiredStates()` Delete expired authorization states (older than 10 minutes). ```typescript const count = await oauth.cleanupExpiredStates(); // Returns: number of deleted states ``` #### ⚙️ Provider Management ##### `registerProvider(name, config)` Register a new OAuth provider. ```typescript oauth.registerProvider('custom', { clientId: 'xxx', clientSecret: 'xxx', authorizationUrl: 'https://provider.com/oauth/authorize', tokenUrl: 'https://provider.com/oauth/token', redirectUri: 'http://localhost:3000/callback', scopes: ['read'], profileUrl?: 'https://provider.com/api/user', // Optional usePKCE?: true, // Optional }); ``` ### Types #### OAuth2Config ```typescript interface OAuth2Config { clientId: string; clientSecret?: string; authorizationUrl: string; tokenUrl: string; redirectUri: string; scopes: string[]; profileUrl?: string; usePKCE?: boolean; extraAuthParams?: Record<string, string>; } ``` #### StoredToken ```typescript interface StoredToken { id: string; provider: string; userId: string; email: string; token: OAuth2Token; metadata?: Record<string, any>; createdAt: Date; updatedAt: Date; } ``` #### OAuth2Token ```typescript interface OAuth2Token { accessToken: string; refreshToken?: string; expiresAt: Date; tokenType: string; scope?: string; } ``` ## 🔌 Storage Adapters ### In-Memory (Default) ```typescript const oauth = new OAuth2Client(); // Uses in-memory storage ``` ### PostgreSQL ```typescript import { PostgresStorageAdapter } from '@dainprotocol/oauth2-storage-postgres'; const storage = new PostgresStorageAdapter({ host: 'localhost', port: 5432, username: 'user', password: 'password', database: 'oauth_tokens', }); const oauth = new OAuth2Client({ storage }); ``` ### Drizzle ORM ```typescript import { DrizzleStorageAdapter } from '@dainprotocol/oauth2-storage-drizzle'; import { drizzle } from 'drizzle-orm/postgres-js'; const db = drizzle(/* your db config */); const storage = new DrizzleStorageAdapter(db, { dialect: 'postgres' }); const oauth = new OAuth2Client({ storage }); ``` ### Custom Profile Fetchers Storage adapters now support registering custom profile fetchers, allowing you to override default behavior or add support for new providers: ```typescript import { BaseProfileFetcher, UserProfile } from '@dainprotocol/oauth2-token-manager'; // Create a custom profile fetcher class CustomProviderFetcher extends BaseProfileFetcher { constructor() { super('https://api.provider.com/user/profile'); } protected mapToUserProfile(rawData: any): UserProfile { return { email: rawData.contact_info.email, name: rawData.full_name, id: rawData.user_id, avatar: rawData.profile_picture, raw: rawData, }; } } // Register with any storage adapter const storage = new InMemoryStorageAdapter(); storage.registerProfileFetcher('custom-provider', new CustomProviderFetcher()); // Or register during Drizzle adapter creation const drizzleStorage = await DrizzleStorageAdapter.create(db, { dialect: 'postgres', profileFetchers: { 'custom-provider': new CustomProviderFetcher(), github: new EnhancedGitHubFetcher(), // Override default GitHub fetcher }, }); const oauth = new OAuth2Client({ storage: drizzleStorage }); ``` ## 📝 Examples ### Express.js Integration ```typescript import express from 'express'; import { OAuth2Client } from '@dainprotocol/oauth2-token-manager'; const app = express(); const oauth = new OAuth2Client({ providers: { google: { clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth', tokenUrl: 'https://oauth2.googleapis.com/token', redirectUri: 'http://localhost:3000/auth/google/callback', scopes: ['profile', 'email'], }, }, }); // Start OAuth flow app.get('/auth/:provider', async (req, res) => { const { url, state } = await oauth.authorize({ provider: req.params.provider, userId: req.user.id, email: req.user.email, }); req.session.oauthState = state; res.redirect(url); }); // Handle callback app.get('/auth/:provider/callback', async (req, res) => { const { code } = req.query; const { token, profile } = await oauth.handleCallback(code, req.session.oauthState); res.json({ success: true, profile }); }); // Use tokens app.get('/api/data', async (req, res) => { const accessToken = await oauth.getAccessToken('google', req.user.email); // Use accessToken for API calls... }); ``` ### Scheduled Cleanup ```typescript // Clean up expired tokens daily setInterval( async () => { const tokens = await oauth.cleanupExpiredTokens(); const states = await oauth.cleanupExpiredStates(); console.log(`Cleaned up ${tokens} tokens and ${states} states`); }, 24 * 60 * 60 * 1000, ); ``` ### Multiple Providers ```typescript // User can connect multiple OAuth accounts const providers = ['google', 'github', 'microsoft']; for (const provider of providers) { const { url, state } = await oauth.authorize({ provider, userId: 'user123', email: 'user@example.com', }); // Handle each provider... } // Get all connected accounts const tokens = await oauth.getTokensByUserId('user123'); console.log( 'Connected accounts:', tokens.map((t) => t.provider), ); ``` ### Custom Profile Fetcher Example ```typescript import { BaseProfileFetcher, UserProfile, GenericProfileFetcher, } from '@dainprotocol/oauth2-token-manager'; // Example 1: Custom fetcher for a proprietary API class CompanyInternalFetcher extends BaseProfileFetcher { constructor() { super('https://internal.company.com/api/v2/me'); } protected mapToUserProfile(rawData: any): UserProfile { return { email: rawData.work_email, name: `${rawData.first_name} ${rawData.last_name}`, id: rawData.employee_id, avatar: rawData.profile_image_url, username: rawData.slack_handle, raw: rawData, }; } protected getAdditionalHeaders(): Record<string, string> { return { 'X-Company-API-Version': '2.0', Accept: 'application/vnd.company+json', }; } } // Example 2: Using GenericProfileFetcher for simple mappings const linkedinFetcher = new GenericProfileFetcher('https://api.linkedin.com/v2/me', { email: 'email_address', name: 'localizedFirstName', id: 'id', avatar: 'profilePicture.displayImage', }); // Example 3: Multiple storage instances with different fetchers const productionStorage = await DrizzleStorageAdapter.create(db, { dialect: 'postgres', profileFetchers: { company: new CompanyInternalFetcher(), linkedin: linkedinFetcher, }, }); const testStorage = await DrizzleStorageAdapter.create(testDb, { dialect: 'postgres', profileFetchers: { company: new MockCompanyFetcher(), // Different fetcher for testing linkedin: new MockLinkedInFetcher(), }, }); ``` ## 📄 License MIT © Dain Protocol