Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

@dainprotocol/oauth2-token-manager

dainprotocol709MIT0.2.0TypeScript support: included

A scalable OAuth2 token management library with multi-system support

oauth2, authentication, token-management, typescript

readme

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

  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.

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