@dainprotocol/oauth2-token-manager 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +786 -0
- package/dist/index.cjs +3 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +785 -0
- package/dist/index.d.ts +785 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/package.json +99 -0
package/README.md
ADDED
|
@@ -0,0 +1,786 @@
|
|
|
1
|
+
# OAuth2 Token Manager
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
## ๐ Features
|
|
6
|
+
|
|
7
|
+
- **๐ Storage Agnostic**: Use any storage backend (In-Memory, PostgreSQL, or build your own adapter)
|
|
8
|
+
- **๐ข Multi-System Support**: Manage tokens across multiple applications/systems
|
|
9
|
+
- **๐ Advanced Security**: PKCE support, state validation, token encryption
|
|
10
|
+
- **โก High Performance**: Efficient token validation, caching, and refresh strategies
|
|
11
|
+
- **๐ Auto-Refresh**: Automatic token refresh with configurable buffers
|
|
12
|
+
- **๐ค User Management**: Comprehensive user lifecycle with email/external ID support
|
|
13
|
+
- **๐ง Profile Integration**: Automatic profile fetching from OAuth providers
|
|
14
|
+
- **๐ฏ Flexible Scoping**: Fine-grained permission management
|
|
15
|
+
- **๐ก Developer Friendly**: Both context-managed and granular APIs
|
|
16
|
+
- **๐งช Fully Tested**: Comprehensive test coverage with Vitest
|
|
17
|
+
|
|
18
|
+
## ๐ฆ Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @dainprotocol/oauth2-token-manager
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Storage Adapters
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# PostgreSQL adapter
|
|
28
|
+
npm install @dainprotocol/oauth2-storage-postgres
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## ๐ Quick Start
|
|
32
|
+
|
|
33
|
+
### Simple Setup
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import { OAuth2Client } from '@dainprotocol/oauth2-token-manager';
|
|
37
|
+
|
|
38
|
+
// Quick setup for common use cases
|
|
39
|
+
const oauth = await OAuth2Client.quickSetup('MyApp', {
|
|
40
|
+
google: {
|
|
41
|
+
clientId: 'your-google-client-id',
|
|
42
|
+
clientSecret: 'your-google-client-secret',
|
|
43
|
+
authorizationUrl: 'https://accounts.google.com/o/oauth2/auth',
|
|
44
|
+
tokenUrl: 'https://oauth2.googleapis.com/token',
|
|
45
|
+
redirectUri: 'http://localhost:3000/auth/callback',
|
|
46
|
+
scopes: ['profile', 'email'],
|
|
47
|
+
},
|
|
48
|
+
github: {
|
|
49
|
+
clientId: 'your-github-client-id',
|
|
50
|
+
clientSecret: 'your-github-client-secret',
|
|
51
|
+
authorizationUrl: 'https://github.com/login/oauth/authorize',
|
|
52
|
+
tokenUrl: 'https://github.com/login/oauth/access_token',
|
|
53
|
+
redirectUri: 'http://localhost:3000/auth/callback',
|
|
54
|
+
scopes: ['user:email'],
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Create or get a user
|
|
59
|
+
const user = await oauth.getOrCreateUser({
|
|
60
|
+
email: 'user@example.com',
|
|
61
|
+
metadata: { role: 'user' },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Start OAuth flow
|
|
65
|
+
const { url, state } = await oauth.authorize({
|
|
66
|
+
provider: 'google',
|
|
67
|
+
scopes: ['profile', 'email'],
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Handle callback
|
|
71
|
+
const result = await oauth.handleCallback(code, state);
|
|
72
|
+
console.log('User authenticated:', result.userId);
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Advanced Setup with Custom Storage
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import { OAuth2Client } from '@dainprotocol/oauth2-token-manager';
|
|
79
|
+
import { PostgresStorageFactory } from '@dainprotocol/oauth2-storage-postgres';
|
|
80
|
+
|
|
81
|
+
// Custom storage adapter
|
|
82
|
+
const storage = await PostgresStorageFactory.create({
|
|
83
|
+
host: 'localhost',
|
|
84
|
+
port: 5432,
|
|
85
|
+
username: 'oauth2_user',
|
|
86
|
+
password: 'secure_password',
|
|
87
|
+
database: 'oauth2_db',
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const oauth = new OAuth2Client({
|
|
91
|
+
storage,
|
|
92
|
+
providers: {
|
|
93
|
+
google: {
|
|
94
|
+
/* config */
|
|
95
|
+
},
|
|
96
|
+
github: {
|
|
97
|
+
/* config */
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Create system and scopes
|
|
103
|
+
const system = await oauth.createSystem('MyApp');
|
|
104
|
+
const scope = await oauth.createScope('api-access', {
|
|
105
|
+
type: 'access',
|
|
106
|
+
permissions: ['read:profile', 'write:data'],
|
|
107
|
+
isolated: true,
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## ๐๏ธ Architecture
|
|
112
|
+
|
|
113
|
+
### Core Components
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
117
|
+
โ OAuth2Client โ
|
|
118
|
+
โ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
|
|
119
|
+
โ โ Context API โ โ Granular API โ โ
|
|
120
|
+
โ โ (Simplified) โ โ (Full Control) โ โ
|
|
121
|
+
โ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
|
|
122
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
123
|
+
โ
|
|
124
|
+
โโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโ
|
|
125
|
+
โ โ โ
|
|
126
|
+
โโโโโโโโโโโผโโโโโโโโโ โโโโโผโโโโโโโโโ โโโโโผโโโโโโโโโโ
|
|
127
|
+
โ Providers โ โ Storage โ โ Profile โ
|
|
128
|
+
โ (OAuth2) โ โ Adapter โ โ Fetchers โ
|
|
129
|
+
โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Data Model & Token Hierarchy
|
|
133
|
+
|
|
134
|
+
**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.
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
// Systems: Top-level applications/services
|
|
138
|
+
interface System {
|
|
139
|
+
id: string;
|
|
140
|
+
name: string;
|
|
141
|
+
description?: string;
|
|
142
|
+
scopes: Scope[];
|
|
143
|
+
metadata?: Record<string, any>;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Scopes: Permission boundaries within systems
|
|
147
|
+
interface Scope {
|
|
148
|
+
id: string;
|
|
149
|
+
systemId: string;
|
|
150
|
+
name: string;
|
|
151
|
+
type: 'authentication' | 'access' | 'custom';
|
|
152
|
+
permissions: string[];
|
|
153
|
+
isolated: boolean; // Whether tokens are isolated to this scope
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Users: Identity within a system
|
|
157
|
+
interface User {
|
|
158
|
+
id: string;
|
|
159
|
+
systemId: string;
|
|
160
|
+
metadata?: Record<string, any>;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// User Tokens: OAuth2 tokens tied to user/system/scope/provider
|
|
164
|
+
// A user can have MULTIPLE tokens for the same provider/scope combination
|
|
165
|
+
interface UserToken {
|
|
166
|
+
id: string;
|
|
167
|
+
userId: string;
|
|
168
|
+
systemId: string;
|
|
169
|
+
scopeId: string;
|
|
170
|
+
provider: string;
|
|
171
|
+
token: OAuth2Token;
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Token Hierarchy Rules
|
|
176
|
+
|
|
177
|
+
1. **One User** belongs to **One System**
|
|
178
|
+
2. **One User** can have tokens in **Multiple Scopes** within their system
|
|
179
|
+
3. **One User** in **One Scope** can have tokens from **Multiple Providers**
|
|
180
|
+
4. **One User** in **One Scope** from **One Provider** can have **Multiple Tokens**
|
|
181
|
+
5. **Email Uniqueness**: For the same provider, a user cannot have multiple tokens with the same email (validated via profile fetcher)
|
|
182
|
+
6. **Cross-Provider Emails**: The same email can exist across different providers
|
|
183
|
+
|
|
184
|
+
## ๐ API Reference
|
|
185
|
+
|
|
186
|
+
### OAuth2Client
|
|
187
|
+
|
|
188
|
+
The main client class providing both context-managed and granular APIs.
|
|
189
|
+
|
|
190
|
+
#### Context-Managed API (Recommended)
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
// System management
|
|
194
|
+
await oauth.createSystem('MyApp');
|
|
195
|
+
await oauth.useSystem(systemId);
|
|
196
|
+
|
|
197
|
+
// User management
|
|
198
|
+
const user = await oauth.getOrCreateUser({ email: 'user@example.com' });
|
|
199
|
+
await oauth.useUser(userId);
|
|
200
|
+
|
|
201
|
+
// Authorization flow
|
|
202
|
+
const { url, state } = await oauth.authorize({ provider: 'google' });
|
|
203
|
+
const result = await oauth.handleCallback(code, state);
|
|
204
|
+
|
|
205
|
+
// Token operations (uses current context + default scope)
|
|
206
|
+
// Note: When multiple tokens exist, these methods use the first (most recent) token
|
|
207
|
+
const accessToken = await oauth.getAccessToken('google');
|
|
208
|
+
const validToken = await oauth.ensureValidToken('google');
|
|
209
|
+
|
|
210
|
+
// Get all user tokens with auto-refresh (for current user)
|
|
211
|
+
const userTokens = await oauth.getUserTokens();
|
|
212
|
+
|
|
213
|
+
// Get all valid tokens for a specific user
|
|
214
|
+
const allTokens = await oauth.getAllValidTokensForUser(userId);
|
|
215
|
+
// Returns: { provider: string; scopeId: string; token: OAuth2Token; userToken: UserToken }[]
|
|
216
|
+
|
|
217
|
+
// Revoke tokens (uses current context)
|
|
218
|
+
await oauth.revokeTokens('google'); // Revokes for current user/scope/provider
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
#### Granular API (Advanced)
|
|
222
|
+
|
|
223
|
+
The granular API provides full control over the token hierarchy:
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
// === User-Centric Token Queries (Primary Key: User) ===
|
|
227
|
+
|
|
228
|
+
// Get ALL tokens for a user across all scopes/providers
|
|
229
|
+
const userTokens = await oauth.granular.getTokensByUser(userId);
|
|
230
|
+
|
|
231
|
+
// Get tokens for user in specific scope (across all providers)
|
|
232
|
+
const scopeTokens = await oauth.granular.getTokensByUserAndScope(userId, scopeId);
|
|
233
|
+
|
|
234
|
+
// Get tokens for user with specific provider (across all scopes)
|
|
235
|
+
const providerTokens = await oauth.granular.getTokensByUserAndProvider(userId, 'google');
|
|
236
|
+
|
|
237
|
+
// Get tokens for user/scope/provider combination (can be multiple!)
|
|
238
|
+
const specificTokens = await oauth.granular.getTokensByUserScopeProvider(userId, scopeId, 'google');
|
|
239
|
+
|
|
240
|
+
// === Cross-User Queries (System/Scope Level) ===
|
|
241
|
+
|
|
242
|
+
// Get all tokens in a scope across all users
|
|
243
|
+
const scopeAllTokens = await oauth.granular.getTokensByScope(systemId, scopeId);
|
|
244
|
+
|
|
245
|
+
// Get all tokens for a provider across all users in system
|
|
246
|
+
const providerAllTokens = await oauth.granular.getTokensByProvider(systemId, 'google');
|
|
247
|
+
|
|
248
|
+
// Get all tokens in a system
|
|
249
|
+
const systemTokens = await oauth.granular.getTokensBySystem(systemId);
|
|
250
|
+
|
|
251
|
+
// === Email-Based Queries ===
|
|
252
|
+
|
|
253
|
+
// Find tokens by email (cross-user, cross-provider)
|
|
254
|
+
const emailTokens = await oauth.granular.findTokensByEmail('user@example.com', systemId);
|
|
255
|
+
|
|
256
|
+
// Find tokens by email in specific scope
|
|
257
|
+
const emailScopeTokens = await oauth.granular.findTokensByEmailAndScope(
|
|
258
|
+
'user@example.com',
|
|
259
|
+
systemId,
|
|
260
|
+
scopeId,
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
// Find tokens by email for specific provider
|
|
264
|
+
const emailProviderTokens = await oauth.granular.findTokensByEmailAndProvider(
|
|
265
|
+
'user@example.com',
|
|
266
|
+
systemId,
|
|
267
|
+
'google',
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
// Find specific token by email/scope/provider (returns single token or null)
|
|
271
|
+
const specificToken = await oauth.granular.findTokenByEmailScopeProvider(
|
|
272
|
+
'user@example.com',
|
|
273
|
+
systemId,
|
|
274
|
+
scopeId,
|
|
275
|
+
'google',
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
// === Token Operations ===
|
|
279
|
+
|
|
280
|
+
// Get valid token for user (auto-refresh, takes first if multiple exist)
|
|
281
|
+
const validToken = await oauth.granular.getValidTokenForUser(userId, scopeId, 'google');
|
|
282
|
+
|
|
283
|
+
// Get access token for user (convenience method)
|
|
284
|
+
const accessToken = await oauth.granular.getAccessTokenForUser(userId, scopeId, 'google');
|
|
285
|
+
|
|
286
|
+
// Save new token for user
|
|
287
|
+
const savedToken = await oauth.granular.saveTokenForUser(
|
|
288
|
+
userId,
|
|
289
|
+
systemId,
|
|
290
|
+
scopeId,
|
|
291
|
+
'google',
|
|
292
|
+
'user@example.com',
|
|
293
|
+
oauthToken,
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
// === Token Management ===
|
|
297
|
+
|
|
298
|
+
// Delete tokens by different criteria
|
|
299
|
+
await oauth.granular.deleteTokensByUser(userId); // All tokens for user
|
|
300
|
+
await oauth.granular.deleteTokensByUserAndScope(userId, scopeId); // User's tokens in scope
|
|
301
|
+
await oauth.granular.deleteTokensByUserAndProvider(userId, 'google'); // User's tokens for provider
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Storage Adapters
|
|
305
|
+
|
|
306
|
+
#### Built-in Memory Adapter
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
import { InMemoryStorageAdapter } from '@dainprotocol/oauth2-token-manager';
|
|
310
|
+
|
|
311
|
+
const storage = new InMemoryStorageAdapter();
|
|
312
|
+
const oauth = new OAuth2Client({ storage });
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
#### PostgreSQL Adapter
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
import { PostgresStorageFactory } from '@dainprotocol/oauth2-storage-postgres';
|
|
319
|
+
|
|
320
|
+
const storage = await PostgresStorageFactory.create({
|
|
321
|
+
host: process.env.DB_HOST,
|
|
322
|
+
port: parseInt(process.env.DB_PORT || '5432'),
|
|
323
|
+
username: process.env.DB_USER,
|
|
324
|
+
password: process.env.DB_PASSWORD,
|
|
325
|
+
database: process.env.DB_NAME,
|
|
326
|
+
ssl: process.env.NODE_ENV === 'production',
|
|
327
|
+
});
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
#### Custom Storage Adapter
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
import { StorageAdapter } from '@dainprotocol/oauth2-token-manager';
|
|
334
|
+
|
|
335
|
+
class MyCustomAdapter implements StorageAdapter {
|
|
336
|
+
async createSystem(system) {
|
|
337
|
+
/* implement */
|
|
338
|
+
}
|
|
339
|
+
async getSystem(id) {
|
|
340
|
+
/* implement */
|
|
341
|
+
}
|
|
342
|
+
// ... implement all required methods
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Provider Configuration
|
|
347
|
+
|
|
348
|
+
#### Google OAuth2
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
{
|
|
352
|
+
google: {
|
|
353
|
+
clientId: 'your-client-id',
|
|
354
|
+
clientSecret: 'your-client-secret',
|
|
355
|
+
authorizationUrl: 'https://accounts.google.com/o/oauth2/auth',
|
|
356
|
+
tokenUrl: 'https://oauth2.googleapis.com/token',
|
|
357
|
+
redirectUri: 'http://localhost:3000/auth/callback',
|
|
358
|
+
scopes: ['profile', 'email'],
|
|
359
|
+
profileUrl: 'https://www.googleapis.com/oauth2/v2/userinfo',
|
|
360
|
+
usePKCE: true, // Recommended for security
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
#### GitHub OAuth2
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
{
|
|
369
|
+
github: {
|
|
370
|
+
clientId: 'your-client-id',
|
|
371
|
+
clientSecret: 'your-client-secret',
|
|
372
|
+
authorizationUrl: 'https://github.com/login/oauth/authorize',
|
|
373
|
+
tokenUrl: 'https://github.com/login/oauth/access_token',
|
|
374
|
+
redirectUri: 'http://localhost:3000/auth/callback',
|
|
375
|
+
scopes: ['user:email'],
|
|
376
|
+
profileUrl: 'https://api.github.com/user',
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
#### Generic Provider
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
{
|
|
385
|
+
custom: {
|
|
386
|
+
clientId: 'your-client-id',
|
|
387
|
+
clientSecret: 'your-client-secret',
|
|
388
|
+
authorizationUrl: 'https://provider.com/oauth/authorize',
|
|
389
|
+
tokenUrl: 'https://provider.com/oauth/token',
|
|
390
|
+
redirectUri: 'http://localhost:3000/auth/callback',
|
|
391
|
+
scopes: ['read', 'write'],
|
|
392
|
+
profileUrl: 'https://provider.com/api/user',
|
|
393
|
+
additionalParams: {
|
|
394
|
+
audience: 'api.example.com'
|
|
395
|
+
},
|
|
396
|
+
responseRootKey: 'data' // For nested responses
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
#### Google OAuth2 with Offline Access
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
const oauth = new OAuth2Client({
|
|
405
|
+
providers: {
|
|
406
|
+
google: {
|
|
407
|
+
clientId: 'your-client-id',
|
|
408
|
+
clientSecret: 'your-client-secret',
|
|
409
|
+
redirectUri: 'http://localhost:3000/auth/callback',
|
|
410
|
+
scopes: ['profile', 'email'],
|
|
411
|
+
// Override default offline access parameters
|
|
412
|
+
extraAuthParams: {
|
|
413
|
+
access_type: 'offline', // Request refresh token
|
|
414
|
+
prompt: 'consent', // Force consent screen
|
|
415
|
+
include_granted_scopes: 'true' // Include previously granted scopes
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// The library automatically handles refresh tokens
|
|
422
|
+
const token = await oauth.getAccessToken('google', {
|
|
423
|
+
autoRefresh: true,
|
|
424
|
+
refreshBuffer: 5 // Refresh 5 minutes before expiry
|
|
425
|
+
});
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
#### Customizing Authorization Parameters
|
|
429
|
+
|
|
430
|
+
Each provider supports customization through `extraAuthParams` and `additionalParams`:
|
|
431
|
+
|
|
432
|
+
```typescript
|
|
433
|
+
{
|
|
434
|
+
google: {
|
|
435
|
+
// ... other config ...
|
|
436
|
+
extraAuthParams: {
|
|
437
|
+
access_type: 'offline', // For refresh tokens
|
|
438
|
+
prompt: 'select_account', // Force account selection
|
|
439
|
+
hd: 'yourdomain.com' // Limit to specific Google Workspace domain
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
microsoft: {
|
|
443
|
+
// ... other config ...
|
|
444
|
+
extraAuthParams: {
|
|
445
|
+
prompt: 'select_account',
|
|
446
|
+
domain_hint: 'yourdomain.com'
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
Available parameters for Google OAuth2:
|
|
453
|
+
- `access_type`: 'online' (default) or 'offline' (for refresh tokens)
|
|
454
|
+
- `prompt`: 'none', 'consent', 'select_account'
|
|
455
|
+
- `include_granted_scopes`: 'true' or 'false'
|
|
456
|
+
- `login_hint`: User's email address
|
|
457
|
+
- `hd`: Google Workspace domain restriction
|
|
458
|
+
|
|
459
|
+
## ๐ง Advanced Features
|
|
460
|
+
|
|
461
|
+
### Token Auto-Refresh
|
|
462
|
+
|
|
463
|
+
```typescript
|
|
464
|
+
const accessToken = await oauth.getAccessToken('google', {
|
|
465
|
+
autoRefresh: true,
|
|
466
|
+
refreshBuffer: 5, // Refresh 5 minutes before expiry
|
|
467
|
+
expirationBuffer: 30, // Consider expired 30 seconds early
|
|
468
|
+
});
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
### Profile-Based Token Management
|
|
472
|
+
|
|
473
|
+
```typescript
|
|
474
|
+
const result = await oauth.handleCallback(code, state, {
|
|
475
|
+
profileOptions: {
|
|
476
|
+
checkProfileEmail: true, // Fetch and check email conflicts
|
|
477
|
+
replaceConflictingTokens: true, // Replace existing tokens with same email
|
|
478
|
+
mergeUserData: true, // Merge profile data into user metadata
|
|
479
|
+
},
|
|
480
|
+
});
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
### Email-Based Operations
|
|
484
|
+
|
|
485
|
+
```typescript
|
|
486
|
+
// Get all valid tokens for an email across all providers in a system
|
|
487
|
+
// Note: This returns an array since one email can have tokens from multiple providers
|
|
488
|
+
const emailTokens = await oauth.getAllValidTokensForEmail('user@example.com', systemId);
|
|
489
|
+
// Returns: { provider: string; scopeId: string; token: OAuth2Token; userToken: UserToken }[]
|
|
490
|
+
|
|
491
|
+
// Get specific token by email (returns single token or null)
|
|
492
|
+
// This enforces the email uniqueness rule within provider/scope
|
|
493
|
+
const token = await oauth.getTokenForEmail('user@example.com', systemId, scopeId, 'google');
|
|
494
|
+
|
|
495
|
+
// Get valid token for email with auto-refresh
|
|
496
|
+
const validToken = await oauth.getValidTokenForEmail(
|
|
497
|
+
'user@example.com',
|
|
498
|
+
systemId,
|
|
499
|
+
scopeId,
|
|
500
|
+
'google',
|
|
501
|
+
{ autoRefresh: true },
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
// Get access token for email
|
|
505
|
+
const accessToken = await oauth.getAccessTokenForEmail(
|
|
506
|
+
'user@example.com',
|
|
507
|
+
systemId,
|
|
508
|
+
scopeId,
|
|
509
|
+
'google',
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
// Execute with valid token for email
|
|
513
|
+
await oauth.withValidTokenForEmail(
|
|
514
|
+
'user@example.com',
|
|
515
|
+
systemId,
|
|
516
|
+
scopeId,
|
|
517
|
+
'google',
|
|
518
|
+
async (accessToken) => {
|
|
519
|
+
console.log('Using token for email:', accessToken);
|
|
520
|
+
},
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
// Check if email has token for specific provider/scope
|
|
524
|
+
const hasToken = await oauth.hasTokenForEmail('user@example.com', systemId, scopeId, 'google');
|
|
525
|
+
|
|
526
|
+
// Revoke tokens for email
|
|
527
|
+
await oauth.revokeTokensForEmail('user@example.com', systemId, scopeId, 'google');
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
### User-Centric Operations (Stateless)
|
|
531
|
+
|
|
532
|
+
For backend APIs where you have explicit user IDs:
|
|
533
|
+
|
|
534
|
+
```typescript
|
|
535
|
+
// Get access token for specific user/scope/provider
|
|
536
|
+
// Note: Takes the first (most recent) token if multiple exist
|
|
537
|
+
const accessToken = await oauth.getAccessTokenForUser(userId, systemId, scopeId, 'google', {
|
|
538
|
+
autoRefresh: true,
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
// Execute with valid token for specific user
|
|
542
|
+
await oauth.withValidTokenForUser(userId, systemId, scopeId, 'google', async (accessToken) => {
|
|
543
|
+
// Make API calls with the token
|
|
544
|
+
return apiResponse;
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
// Get all valid tokens for a user with auto-refresh
|
|
548
|
+
const userTokens = await oauth.getAllValidTokensForUser(userId, {
|
|
549
|
+
autoRefresh: true,
|
|
550
|
+
refreshBuffer: 5, // Refresh 5 minutes before expiry
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
// Check if user has tokens for specific provider/scope
|
|
554
|
+
const hasToken = await oauth.hasTokenForUser(userId, systemId, scopeId, 'google');
|
|
555
|
+
|
|
556
|
+
// Get user token entity (includes metadata)
|
|
557
|
+
const userToken = await oauth.getUserTokenForUser(userId, systemId, scopeId, 'google');
|
|
558
|
+
|
|
559
|
+
// Revoke specific tokens
|
|
560
|
+
await oauth.revokeTokensForUser(userId, systemId, scopeId, 'google');
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
### PKCE Support
|
|
564
|
+
|
|
565
|
+
```typescript
|
|
566
|
+
// Enable PKCE for enhanced security
|
|
567
|
+
const { url, state } = await oauth.authorize({
|
|
568
|
+
provider: 'google',
|
|
569
|
+
usePKCE: true, // Enables PKCE flow
|
|
570
|
+
});
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
### Token Validation
|
|
574
|
+
|
|
575
|
+
```typescript
|
|
576
|
+
// Check if token is expired
|
|
577
|
+
const isExpired = oauth.isTokenExpired(token, {
|
|
578
|
+
expirationBuffer: 60, // Consider expired 60 seconds early
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
// Ensure valid token (auto-refresh if needed)
|
|
582
|
+
const validToken = await oauth.ensureValidToken('google');
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
## ๐ Security Features
|
|
586
|
+
|
|
587
|
+
### State Management
|
|
588
|
+
|
|
589
|
+
- Cryptographically secure state generation
|
|
590
|
+
- Automatic state validation and cleanup
|
|
591
|
+
- Configurable state expiration
|
|
592
|
+
|
|
593
|
+
### PKCE (Proof Key for Code Exchange)
|
|
594
|
+
|
|
595
|
+
- Built-in PKCE support for public clients
|
|
596
|
+
- Automatic code verifier generation
|
|
597
|
+
- Enhanced security for mobile and SPA applications
|
|
598
|
+
|
|
599
|
+
### Token Encryption
|
|
600
|
+
|
|
601
|
+
- Secure token storage with optional encryption
|
|
602
|
+
- Configurable seal keys for sensitive data
|
|
603
|
+
- Protection against token theft
|
|
604
|
+
|
|
605
|
+
### Email Validation
|
|
606
|
+
|
|
607
|
+
- Automatic email conflict detection
|
|
608
|
+
- Profile-based user validation
|
|
609
|
+
- Cross-provider email consistency
|
|
610
|
+
|
|
611
|
+
## ๐งช Testing
|
|
612
|
+
|
|
613
|
+
The library includes comprehensive tests using Vitest:
|
|
614
|
+
|
|
615
|
+
```bash
|
|
616
|
+
# Run tests
|
|
617
|
+
npm test
|
|
618
|
+
|
|
619
|
+
# Run tests with UI
|
|
620
|
+
npm run test:ui
|
|
621
|
+
|
|
622
|
+
# Run tests with coverage
|
|
623
|
+
npm run test:coverage
|
|
624
|
+
|
|
625
|
+
# Watch mode
|
|
626
|
+
npm run test:watch
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
## ๐ข Multi-System Examples
|
|
630
|
+
|
|
631
|
+
### SaaS Platform with Multiple Apps
|
|
632
|
+
|
|
633
|
+
```typescript
|
|
634
|
+
// Create systems for different applications
|
|
635
|
+
const crmSystem = await oauth.createSystem('CRM App');
|
|
636
|
+
const analyticsSystem = await oauth.createSystem('Analytics Dashboard');
|
|
637
|
+
|
|
638
|
+
// Create scopes for different access levels
|
|
639
|
+
await oauth.useSystem(crmSystem.id);
|
|
640
|
+
const readScope = await oauth.createScope('read-only', {
|
|
641
|
+
type: 'access',
|
|
642
|
+
permissions: ['read:contacts', 'read:deals'],
|
|
643
|
+
isolated: true,
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
const adminScope = await oauth.createScope('admin', {
|
|
647
|
+
type: 'access',
|
|
648
|
+
permissions: ['*'],
|
|
649
|
+
isolated: true,
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
// Users can have different permissions per system
|
|
653
|
+
const user = await oauth.getOrCreateUser({ email: 'user@company.com' });
|
|
654
|
+
|
|
655
|
+
// Authorize for specific system/scope
|
|
656
|
+
const { url } = await oauth.authorize({
|
|
657
|
+
provider: 'google',
|
|
658
|
+
scopes: ['profile', 'email'],
|
|
659
|
+
});
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
### Multi-Tenant Application
|
|
663
|
+
|
|
664
|
+
```typescript
|
|
665
|
+
// Each tenant gets their own system
|
|
666
|
+
const tenantSystem = await oauth.createSystem(`Tenant-${tenantId}`);
|
|
667
|
+
|
|
668
|
+
// Tenant-specific user management
|
|
669
|
+
await oauth.useSystem(tenantSystem.id);
|
|
670
|
+
const tenantUser = await oauth.getOrCreateUser({
|
|
671
|
+
email: userEmail,
|
|
672
|
+
metadata: { tenantId, role: 'admin' },
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
// Tenant-isolated tokens
|
|
676
|
+
const tokens = await oauth.granular.getTokensBySystem(tenantSystem.id);
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
## ๐ Production Deployment
|
|
680
|
+
|
|
681
|
+
### Environment Configuration
|
|
682
|
+
|
|
683
|
+
```typescript
|
|
684
|
+
const oauth = new OAuth2Client({
|
|
685
|
+
storage: await PostgresStorageFactory.create({
|
|
686
|
+
host: process.env.DB_HOST,
|
|
687
|
+
port: parseInt(process.env.DB_PORT || '5432'),
|
|
688
|
+
username: process.env.DB_USER,
|
|
689
|
+
password: process.env.DB_PASSWORD,
|
|
690
|
+
database: process.env.DB_NAME,
|
|
691
|
+
ssl: {
|
|
692
|
+
rejectUnauthorized: process.env.NODE_ENV === 'production',
|
|
693
|
+
},
|
|
694
|
+
poolSize: 20,
|
|
695
|
+
logging: process.env.NODE_ENV === 'development',
|
|
696
|
+
}),
|
|
697
|
+
sealKey: process.env.OAUTH2_SEAL_KEY, // For token encryption
|
|
698
|
+
providers: {
|
|
699
|
+
google: {
|
|
700
|
+
clientId: process.env.GOOGLE_CLIENT_ID,
|
|
701
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
702
|
+
redirectUri: process.env.GOOGLE_REDIRECT_URI,
|
|
703
|
+
// ... other config
|
|
704
|
+
},
|
|
705
|
+
},
|
|
706
|
+
});
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
### Performance Optimization
|
|
710
|
+
|
|
711
|
+
```typescript
|
|
712
|
+
// Use token caching for high-traffic scenarios
|
|
713
|
+
const accessToken = await oauth.getAccessToken('google', {
|
|
714
|
+
autoRefresh: true,
|
|
715
|
+
refreshBuffer: 10, // Refresh early to avoid expiry
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
// Batch operations for efficiency
|
|
719
|
+
const allTokens = await oauth.getAllValidTokensForUser(userId);
|
|
720
|
+
|
|
721
|
+
// Clean up expired states regularly
|
|
722
|
+
setInterval(
|
|
723
|
+
async () => {
|
|
724
|
+
await oauth.cleanup(10 * 60 * 1000); // 10 minutes
|
|
725
|
+
},
|
|
726
|
+
5 * 60 * 1000,
|
|
727
|
+
); // Every 5 minutes
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
### Error Handling
|
|
731
|
+
|
|
732
|
+
```typescript
|
|
733
|
+
try {
|
|
734
|
+
const token = await oauth.getAccessToken('google');
|
|
735
|
+
} catch (error) {
|
|
736
|
+
if (error.message.includes('Token expired')) {
|
|
737
|
+
// Handle token expiry
|
|
738
|
+
const { url } = await oauth.authorize({ provider: 'google' });
|
|
739
|
+
// Redirect to re-authorization
|
|
740
|
+
} else if (error.message.includes('Provider not found')) {
|
|
741
|
+
// Handle missing provider
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
## ๐ค Contributing
|
|
747
|
+
|
|
748
|
+
1. Fork the repository
|
|
749
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
750
|
+
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
751
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
752
|
+
5. Open a Pull Request
|
|
753
|
+
|
|
754
|
+
### Development Setup
|
|
755
|
+
|
|
756
|
+
```bash
|
|
757
|
+
# Install dependencies
|
|
758
|
+
npm install
|
|
759
|
+
|
|
760
|
+
# Run in development mode
|
|
761
|
+
npm run dev
|
|
762
|
+
|
|
763
|
+
# Run tests
|
|
764
|
+
npm test
|
|
765
|
+
|
|
766
|
+
# Build the project
|
|
767
|
+
npm run build
|
|
768
|
+
|
|
769
|
+
# Lint and format
|
|
770
|
+
npm run lint:fix
|
|
771
|
+
npm run format
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
## ๐ License
|
|
775
|
+
|
|
776
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
777
|
+
|
|
778
|
+
## ๐โโ๏ธ Support
|
|
779
|
+
|
|
780
|
+
- ๐ [Documentation](https://github.com/blureffect/oauth2-token-manager#readme)
|
|
781
|
+
- ๐ [Issue Tracker](https://github.com/blureffect/oauth2-token-manager/issues)
|
|
782
|
+
- ๐ฌ [Discussions](https://github.com/blureffect/oauth2-token-manager/discussions)
|
|
783
|
+
|
|
784
|
+
## ๐ Credits
|
|
785
|
+
|
|
786
|
+
Created with โค๏ธ by [Blureffect](https://blureffect.co)
|