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
- 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
npm install @dainprotocol/oauth2-token-manager
Storage Adapters
# PostgreSQL adapter (TypeORM based)
npm install @dainprotocol/oauth2-storage-postgres
# Drizzle adapter (supports PostgreSQL, MySQL, SQLite)
npm install @dainprotocol/oauth2-storage-drizzle
🚀 Quick Start
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
- One token per provider/email: Each email can have one token per OAuth provider
- Automatic override: Saving a token with same provider + email replaces the old one
- Auto refresh: Expired tokens refresh automatically when you request them
- Simple storage: Just provider (string), userId (string), and email (string)
📚 API Reference
OAuth2Client
The main class for managing OAuth2 tokens.
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.
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.
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).
const accessToken = await oauth.getAccessToken('google', 'user@example.com');
// Returns: "ya29.a0AfH6SMBx..."
getValidToken(provider, email, options?)
Get full token object (auto-refreshes if expired).
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.
const tokens = await oauth.getTokensByUserId('user123');
// Returns: StoredToken[]
getTokensByEmail(email)
Get all tokens for an email.
const tokens = await oauth.getTokensByEmail('user@example.com');
// Returns: StoredToken[]
🗑️ Token Cleanup
deleteToken(provider, email)
Delete a specific token.
const deleted = await oauth.deleteToken('google', 'user@example.com');
// Returns: boolean
cleanupExpiredTokens()
Delete all expired tokens.
const count = await oauth.cleanupExpiredTokens();
// Returns: number of deleted tokens
cleanupExpiredStates()
Delete expired authorization states (older than 10 minutes).
const count = await oauth.cleanupExpiredStates();
// Returns: number of deleted states
⚙️ Provider Management
registerProvider(name, config)
Register a new OAuth provider.
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
interface OAuth2Config {
clientId: string;
clientSecret?: string;
authorizationUrl: string;
tokenUrl: string;
redirectUri: string;
scopes: string[];
profileUrl?: string;
usePKCE?: boolean;
extraAuthParams?: Record<string, string>;
}
StoredToken
interface StoredToken {
id: string;
provider: string;
userId: string;
email: string;
token: OAuth2Token;
metadata?: Record<string, any>;
createdAt: Date;
updatedAt: Date;
}
OAuth2Token
interface OAuth2Token {
accessToken: string;
refreshToken?: string;
expiresAt: Date;
tokenType: string;
scope?: string;
}
🔌 Storage Adapters
In-Memory (Default)
const oauth = new OAuth2Client(); // Uses in-memory storage
PostgreSQL
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
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:
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
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
// 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
// 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
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