@dainprotocol/oauth2-token-manager
Version:
A scalable OAuth2 token management library with multi-system support
498 lines (388 loc) • 12.4 kB
Markdown
for Node.js. Store and manage OAuth2 tokens with automatic refresh, multiple providers, and pluggable storage.
- [Features](
- [Installation](
- [Quick Start](
- [Core Concepts](
- [API Reference](
- [Storage Adapters](
- [Examples](
- **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
```bash
npm install @dainprotocol/oauth2-token-manager
```
```bash
npm install @dainprotocol/oauth2-storage-postgres
npm install @dainprotocol/oauth2-storage-drizzle
```
```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');
```
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
[ ]: OAuth2Config
}
});
```
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`
```
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
```
Get access token string (auto-refreshes if expired).
```typescript
const accessToken = await oauth.getAccessToken('google', 'user@example.com');
// Returns: "ya29.a0AfH6SMBx..."
```
Get full token object (auto-refreshes if expired).
```typescript
const token = await oauth.getValidToken('google', 'user@example.com');
// Returns: { accessToken, refreshToken, expiresAt, tokenType, scope, ... }
```
Get all tokens for a user.
```typescript
const tokens = await oauth.getTokensByUserId('user123');
// Returns: StoredToken[]
```
Get all tokens for an email.
```typescript
const tokens = await oauth.getTokensByEmail('user@example.com');
// Returns: StoredToken[]
```
Delete a specific token.
```typescript
const deleted = await oauth.deleteToken('google', 'user@example.com');
// Returns: boolean
```
Delete all expired tokens.
```typescript
const count = await oauth.cleanupExpiredTokens();
// Returns: number of deleted tokens
```
Delete expired authorization states (older than 10 minutes).
```typescript
const count = await oauth.cleanupExpiredStates();
// Returns: number of deleted states
```
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
});
```
```typescript
interface OAuth2Config {
clientId: string;
clientSecret?: string;
authorizationUrl: string;
tokenUrl: string;
redirectUri: string;
scopes: string[];
profileUrl?: string;
usePKCE?: boolean;
extraAuthParams?: Record<string, string>;
}
```
```typescript
interface StoredToken {
id: string;
provider: string;
userId: string;
email: string;
token: OAuth2Token;
metadata?: Record<string, any>;
createdAt: Date;
updatedAt: Date;
}
```
```typescript
interface OAuth2Token {
accessToken: string;
refreshToken?: string;
expiresAt: Date;
tokenType: string;
scope?: string;
}
```
```typescript
const oauth = new OAuth2Client(); // Uses in-memory storage
```
```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 });
```
```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 });
```
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 });
```
```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...
});
```
```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,
);
```
```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),
);
```
```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(),
},
});
```
MIT © Dain Protocol
A simple OAuth2 token management library