@blureffect/oauth2-token-manager
Version:
A scalable OAuth2 token management library with multi-system support
788 lines (624 loc) โข 22.9 kB
Markdown
# 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)