@dainprotocol/oauth2-token-manager 0.1.1 โ 0.1.2
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 +224 -694
- package/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +119 -775
- package/dist/index.d.ts +119 -775
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,39 +1,25 @@
|
|
|
1
1
|
# OAuth2 Token Manager
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A simple OAuth2 token management library for Node.js. Store and manage OAuth2 tokens with automatic refresh, multiple providers, and pluggable storage.
|
|
4
4
|
|
|
5
5
|
## ๐ Table of Contents
|
|
6
6
|
|
|
7
7
|
- [Features](#-features)
|
|
8
8
|
- [Installation](#-installation)
|
|
9
9
|
- [Quick Start](#-quick-start)
|
|
10
|
-
- [
|
|
11
|
-
- [Context-Aware Resolution](#context-aware-resolution)
|
|
12
|
-
- [User Management](#user-management)
|
|
13
|
-
- [Token Operations](#token-operations)
|
|
14
|
-
- [Bulk Operations](#bulk-operations)
|
|
15
|
-
- [System & Scope Management](#system--scope-management)
|
|
16
|
-
- [Architecture](#-architecture)
|
|
10
|
+
- [Core Concepts](#-core-concepts)
|
|
17
11
|
- [API Reference](#-api-reference)
|
|
18
|
-
- [Storage Adapters](
|
|
19
|
-
- [Provider Configuration](#provider-configuration)
|
|
20
|
-
- [Advanced Features](#-advanced-features)
|
|
12
|
+
- [Storage Adapters](#-storage-adapters)
|
|
21
13
|
- [Examples](#-examples)
|
|
22
|
-
- [Production Deployment](#-production-deployment)
|
|
23
14
|
|
|
24
15
|
## ๐ Features
|
|
25
16
|
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
- **๐ง Profile Integration**: Automatic profile fetching from OAuth providers
|
|
33
|
-
- **๐ฏ Flexible Scoping**: Fine-grained permission management
|
|
34
|
-
- **๐จ Default Configuration**: Automatic default system and scope creation
|
|
35
|
-
- **๐ก Developer Friendly**: Both context-managed and granular APIs
|
|
36
|
-
- **๐งช Fully Tested**: Comprehensive test coverage with Vitest
|
|
17
|
+
- **Simple Storage**: Tokens stored by provider + email (unique constraint)
|
|
18
|
+
- **Auto Refresh**: Tokens refresh automatically when expired
|
|
19
|
+
- **Multiple Providers**: Google, GitHub, Microsoft, Facebook, and custom providers
|
|
20
|
+
- **Profile Fetching**: Get user profiles during OAuth callback
|
|
21
|
+
- **Pluggable Storage**: In-memory, PostgreSQL, Drizzle, or custom adapters
|
|
22
|
+
- **Type Safe**: Full TypeScript support
|
|
37
23
|
|
|
38
24
|
## ๐ฆ Installation
|
|
39
25
|
|
|
@@ -44,7 +30,7 @@ npm install @dainprotocol/oauth2-token-manager
|
|
|
44
30
|
### Storage Adapters
|
|
45
31
|
|
|
46
32
|
```bash
|
|
47
|
-
# PostgreSQL adapter
|
|
33
|
+
# PostgreSQL adapter (TypeORM based)
|
|
48
34
|
npm install @dainprotocol/oauth2-storage-postgres
|
|
49
35
|
|
|
50
36
|
# Drizzle adapter (supports PostgreSQL, MySQL, SQLite)
|
|
@@ -53,814 +39,358 @@ npm install @dainprotocol/oauth2-storage-drizzle
|
|
|
53
39
|
|
|
54
40
|
## ๐ Quick Start
|
|
55
41
|
|
|
56
|
-
### Simple Setup
|
|
57
|
-
|
|
58
42
|
```typescript
|
|
59
43
|
import { OAuth2Client } from '@dainprotocol/oauth2-token-manager';
|
|
60
44
|
|
|
61
|
-
//
|
|
62
|
-
const oauth =
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
45
|
+
// Initialize with provider configurations
|
|
46
|
+
const oauth = new OAuth2Client({
|
|
47
|
+
providers: {
|
|
48
|
+
google: {
|
|
49
|
+
clientId: process.env.GOOGLE_CLIENT_ID,
|
|
50
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
51
|
+
authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
|
|
52
|
+
tokenUrl: 'https://oauth2.googleapis.com/token',
|
|
53
|
+
redirectUri: 'http://localhost:3000/auth/google/callback',
|
|
54
|
+
scopes: ['profile', 'email'],
|
|
55
|
+
profileUrl: 'https://www.googleapis.com/oauth2/v2/userinfo',
|
|
56
|
+
},
|
|
57
|
+
github: {
|
|
58
|
+
clientId: process.env.GITHUB_CLIENT_ID,
|
|
59
|
+
clientSecret: process.env.GITHUB_CLIENT_SECRET,
|
|
60
|
+
authorizationUrl: 'https://github.com/login/oauth/authorize',
|
|
61
|
+
tokenUrl: 'https://github.com/login/oauth/access_token',
|
|
62
|
+
redirectUri: 'http://localhost:3000/auth/github/callback',
|
|
63
|
+
scopes: ['user:email'],
|
|
64
|
+
profileUrl: 'https://api.github.com/user',
|
|
65
|
+
},
|
|
78
66
|
},
|
|
79
67
|
});
|
|
80
68
|
|
|
81
|
-
// Create or get a user
|
|
82
|
-
const user = await oauth.getOrCreateUser({
|
|
83
|
-
email: 'user@example.com',
|
|
84
|
-
metadata: { role: 'user' },
|
|
85
|
-
});
|
|
86
|
-
|
|
87
69
|
// Start OAuth flow
|
|
88
70
|
const { url, state } = await oauth.authorize({
|
|
89
71
|
provider: 'google',
|
|
90
|
-
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
// Handle callback
|
|
94
|
-
const result = await oauth.handleCallback(code, state);
|
|
95
|
-
console.log('User authenticated:', result.userId);
|
|
96
|
-
|
|
97
|
-
// === NEW: Simplified Granular API (V2) ===
|
|
98
|
-
// Access tokens without specifying system/scope IDs
|
|
99
|
-
const accessToken = await oauth.granularV2.getAccessToken({
|
|
72
|
+
userId: 'user123',
|
|
100
73
|
email: 'user@example.com',
|
|
101
|
-
|
|
74
|
+
scopes: ['profile', 'email'],
|
|
102
75
|
});
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
## ๐ฏ Granular API (Advanced)
|
|
106
|
-
|
|
107
|
-
The granular API (`oauth.granularV2`) provides full control over the token hierarchy with intelligent system and scope resolution. It's the recommended way to interact with tokens when you need fine-grained control.
|
|
108
|
-
|
|
109
|
-
### Context-Aware Resolution
|
|
110
76
|
|
|
111
|
-
|
|
77
|
+
// Redirect user to authorization URL
|
|
78
|
+
res.redirect(url);
|
|
112
79
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
3. **Default system/scope** - If no context is set, it falls back to the default system and scope
|
|
80
|
+
// Handle OAuth callback
|
|
81
|
+
const { token, profile } = await oauth.handleCallback(code, state);
|
|
116
82
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
// Set context in main client
|
|
121
|
-
await oauth.useSystem('production-system');
|
|
122
|
-
oauth.setDefaultScope('api-access');
|
|
123
|
-
|
|
124
|
-
// Granular V2 will now use this context automatically
|
|
125
|
-
const user = await oauth.granularV2.getOrCreateUser({
|
|
126
|
-
email: 'user@example.com',
|
|
127
|
-
options: { metadata: { role: 'user' } },
|
|
128
|
-
// No need to specify systemId - uses 'production-system' from context
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
// Token operations use the context scope
|
|
132
|
-
const token = await oauth.granularV2.getAccessToken({
|
|
133
|
-
email: 'user@example.com',
|
|
134
|
-
provider: 'google',
|
|
135
|
-
// No need to specify scopeId - uses 'api-access' from context
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
// Override with explicit parameters when needed
|
|
139
|
-
const differentToken = await oauth.granularV2.getAccessToken({
|
|
140
|
-
email: 'user@example.com',
|
|
141
|
-
provider: 'google',
|
|
142
|
-
systemId: 'another-system', // This takes precedence
|
|
143
|
-
scopeId: 'different-scope', // This takes precedence
|
|
144
|
-
});
|
|
83
|
+
// Use the token
|
|
84
|
+
const accessToken = await oauth.getAccessToken('google', 'user@example.com');
|
|
145
85
|
```
|
|
146
86
|
|
|
147
|
-
|
|
87
|
+
## ๐ Core Concepts
|
|
148
88
|
|
|
149
|
-
|
|
150
|
-
// Create or get users with optional system specification
|
|
151
|
-
const user = await oauth.granularV2.getOrCreateUser({
|
|
152
|
-
email: 'user@example.com',
|
|
153
|
-
options: { metadata: { role: 'admin' } },
|
|
154
|
-
// systemId is optional - uses context or defaults
|
|
155
|
-
});
|
|
89
|
+
### How It Works
|
|
156
90
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
91
|
+
1. **One token per provider/email**: Each email can have one token per OAuth provider
|
|
92
|
+
2. **Automatic override**: Saving a token with same provider + email replaces the old one
|
|
93
|
+
3. **Auto refresh**: Expired tokens refresh automatically when you request them
|
|
94
|
+
4. **Simple storage**: Just provider (string), userId (string), and email (string)
|
|
161
95
|
|
|
162
|
-
|
|
163
|
-
externalId: 'external-123',
|
|
164
|
-
});
|
|
96
|
+
## ๐ API Reference
|
|
165
97
|
|
|
166
|
-
|
|
167
|
-
const systemUsers = await oauth.granularV2.getUsersBySystem();
|
|
168
|
-
// Or specify a different system
|
|
169
|
-
const otherSystemUsers = await oauth.granularV2.getUsersBySystem('other-system-id');
|
|
170
|
-
```
|
|
98
|
+
### OAuth2Client
|
|
171
99
|
|
|
172
|
-
|
|
100
|
+
The main class for managing OAuth2 tokens.
|
|
173
101
|
|
|
174
102
|
```typescript
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
token: oauthToken,
|
|
181
|
-
// systemId and scopeId are optional
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
// Query tokens with flexible parameters
|
|
185
|
-
const userTokens = await oauth.granularV2.getTokens({
|
|
186
|
-
userId: user.id,
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
const providerTokens = await oauth.granularV2.getTokens({
|
|
190
|
-
userId: user.id,
|
|
191
|
-
provider: 'google',
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
const emailTokens = await oauth.granularV2.getTokens({
|
|
195
|
-
email: 'user@example.com',
|
|
196
|
-
// systemId resolved from context or defaults
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
// Get valid tokens with auto-refresh
|
|
200
|
-
const validToken = await oauth.granularV2.getValidToken({
|
|
201
|
-
email: 'user@example.com',
|
|
202
|
-
provider: 'google',
|
|
203
|
-
options: {
|
|
204
|
-
autoRefresh: true,
|
|
205
|
-
refreshBuffer: 5, // Refresh 5 minutes before expiry
|
|
206
|
-
},
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
// Get access token directly
|
|
210
|
-
const accessToken = await oauth.granularV2.getAccessToken({
|
|
211
|
-
userId: user.id,
|
|
212
|
-
provider: 'google',
|
|
103
|
+
const oauth = new OAuth2Client({
|
|
104
|
+
storage?: StorageAdapter, // Optional, defaults to in-memory
|
|
105
|
+
providers?: { // OAuth provider configurations
|
|
106
|
+
[name: string]: OAuth2Config
|
|
107
|
+
}
|
|
213
108
|
});
|
|
214
|
-
|
|
215
|
-
// Execute with valid token
|
|
216
|
-
await oauth.granularV2.withValidToken(
|
|
217
|
-
{
|
|
218
|
-
email: 'user@example.com',
|
|
219
|
-
provider: 'google',
|
|
220
|
-
},
|
|
221
|
-
async (accessToken) => {
|
|
222
|
-
// Make API calls with the token
|
|
223
|
-
const response = await fetch('https://api.example.com/data', {
|
|
224
|
-
headers: { Authorization: `Bearer ${accessToken}` },
|
|
225
|
-
});
|
|
226
|
-
return response.json();
|
|
227
|
-
},
|
|
228
|
-
);
|
|
229
109
|
```
|
|
230
110
|
|
|
231
|
-
###
|
|
111
|
+
### Methods
|
|
232
112
|
|
|
233
|
-
|
|
234
|
-
// Get all valid tokens for a user
|
|
235
|
-
const allUserTokens = await oauth.granularV2.getAllValidTokens({
|
|
236
|
-
userId: user.id,
|
|
237
|
-
options: { autoRefresh: true },
|
|
238
|
-
});
|
|
113
|
+
#### ๐ OAuth Flow
|
|
239
114
|
|
|
240
|
-
|
|
241
|
-
const allEmailTokens = await oauth.granularV2.getAllValidTokens({
|
|
242
|
-
email: 'user@example.com',
|
|
243
|
-
});
|
|
115
|
+
##### `authorize(options)`
|
|
244
116
|
|
|
245
|
-
|
|
246
|
-
const googleTokens = await oauth.granularV2.getAllValidTokens({
|
|
247
|
-
email: 'user@example.com',
|
|
248
|
-
provider: 'google',
|
|
249
|
-
});
|
|
117
|
+
Start OAuth2 authorization flow.
|
|
250
118
|
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
email: 'user@example.com',
|
|
119
|
+
```typescript
|
|
120
|
+
const { url, state } = await oauth.authorize({
|
|
254
121
|
provider: 'google',
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
// Delete tokens
|
|
258
|
-
await oauth.granularV2.deleteTokens({
|
|
122
|
+
userId: 'user123',
|
|
259
123
|
email: 'user@example.com',
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
// Delete all tokens for a user
|
|
264
|
-
await oauth.granularV2.deleteTokens({
|
|
265
|
-
userId: user.id,
|
|
124
|
+
scopes?: ['profile', 'email'], // Optional
|
|
125
|
+
usePKCE?: true, // Optional
|
|
126
|
+
metadata?: { source: 'signup' } // Optional
|
|
266
127
|
});
|
|
128
|
+
// Redirect user to `url`
|
|
267
129
|
```
|
|
268
130
|
|
|
269
|
-
|
|
131
|
+
##### `handleCallback(code, state)`
|
|
270
132
|
|
|
271
|
-
|
|
272
|
-
// Get current defaults
|
|
273
|
-
const defaultSystem = await oauth.granularV2.getSystem();
|
|
274
|
-
const defaultScope = await oauth.granularV2.getScope();
|
|
275
|
-
|
|
276
|
-
// Create new scope in current/default system
|
|
277
|
-
const newScope = await oauth.granularV2.createScope({
|
|
278
|
-
name: 'api-access',
|
|
279
|
-
type: 'access',
|
|
280
|
-
permissions: ['read', 'write'],
|
|
281
|
-
isolated: true,
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
// Create scope in specific system
|
|
285
|
-
const anotherScope = await oauth.granularV2.createScope({
|
|
286
|
-
systemId: 'specific-system-id',
|
|
287
|
-
name: 'admin-access',
|
|
288
|
-
type: 'access',
|
|
289
|
-
permissions: ['*'],
|
|
290
|
-
});
|
|
133
|
+
Handle OAuth2 callback and save tokens.
|
|
291
134
|
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
//
|
|
296
|
-
const { system, scope } = await oauth.granularV2.ensureDefaults();
|
|
135
|
+
```typescript
|
|
136
|
+
const { token, profile } = await oauth.handleCallback(code, state);
|
|
137
|
+
// token: { id, provider, userId, email, token, metadata, ... }
|
|
138
|
+
// profile: { id, email, name, picture, raw } or undefined
|
|
297
139
|
```
|
|
298
140
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
The granular API is ideal for:
|
|
302
|
-
|
|
303
|
-
- **Single-system applications** - Simplified usage with automatic defaults
|
|
304
|
-
- **Multi-tenant applications** - Easy context switching between systems
|
|
305
|
-
- **Backend services** - Stateless operations with explicit parameters
|
|
306
|
-
- **Complex token management** - Full control over token lifecycle
|
|
307
|
-
- **Gradual migration** - Start simple, add complexity as needed
|
|
141
|
+
#### ๐ Token Management
|
|
308
142
|
|
|
309
|
-
|
|
143
|
+
##### `getAccessToken(provider, email, options?)`
|
|
310
144
|
|
|
311
|
-
|
|
145
|
+
Get access token string (auto-refreshes if expired).
|
|
312
146
|
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
โ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
|
|
317
|
-
โ โ Context API โ โ Granular API V2 โ โ
|
|
318
|
-
โ โ (Simplified) โ โ (Full Control + Smart) โ โ
|
|
319
|
-
โ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
|
|
320
|
-
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
321
|
-
โ
|
|
322
|
-
โโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโ
|
|
323
|
-
โ โ โ
|
|
324
|
-
โโโโโโโโโโโผโโโโโโโโโ โโโโโผโโโโโโโโโ โโโโโผโโโโโโโโโโ
|
|
325
|
-
โ Providers โ โ Storage โ โ Profile โ
|
|
326
|
-
โ (OAuth2) โ โ Adapter โ โ Fetchers โ
|
|
327
|
-
โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ
|
|
147
|
+
```typescript
|
|
148
|
+
const accessToken = await oauth.getAccessToken('google', 'user@example.com');
|
|
149
|
+
// Returns: "ya29.a0AfH6SMBx..."
|
|
328
150
|
```
|
|
329
151
|
|
|
330
|
-
|
|
152
|
+
##### `getValidToken(provider, email, options?)`
|
|
331
153
|
|
|
332
|
-
|
|
154
|
+
Get full token object (auto-refreshes if expired).
|
|
333
155
|
|
|
334
156
|
```typescript
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
id: string;
|
|
338
|
-
name: string;
|
|
339
|
-
description?: string;
|
|
340
|
-
scopes: Scope[];
|
|
341
|
-
metadata?: Record<string, any>;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// Scopes: Permission boundaries within systems
|
|
345
|
-
interface Scope {
|
|
346
|
-
id: string;
|
|
347
|
-
systemId: string;
|
|
348
|
-
name: string;
|
|
349
|
-
type: 'authentication' | 'access' | 'custom';
|
|
350
|
-
permissions: string[];
|
|
351
|
-
isolated: boolean; // Whether tokens are isolated to this scope
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// Users: Identity within a system
|
|
355
|
-
interface User {
|
|
356
|
-
id: string;
|
|
357
|
-
systemId: string;
|
|
358
|
-
metadata?: Record<string, any>;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// User Tokens: OAuth2 tokens tied to user/system/scope/provider
|
|
362
|
-
// A user can have MULTIPLE tokens for the same provider/scope combination
|
|
363
|
-
interface UserToken {
|
|
364
|
-
id: string;
|
|
365
|
-
userId: string;
|
|
366
|
-
systemId: string;
|
|
367
|
-
scopeId: string;
|
|
368
|
-
provider: string;
|
|
369
|
-
token: OAuth2Token;
|
|
370
|
-
}
|
|
157
|
+
const token = await oauth.getValidToken('google', 'user@example.com');
|
|
158
|
+
// Returns: { accessToken, refreshToken, expiresAt, tokenType, scope, ... }
|
|
371
159
|
```
|
|
372
160
|
|
|
373
|
-
|
|
161
|
+
#### ๐ Token Queries
|
|
374
162
|
|
|
375
|
-
|
|
376
|
-
2. **One User** can have tokens in **Multiple Scopes** within their system
|
|
377
|
-
3. **One User** in **One Scope** can have tokens from **Multiple Providers**
|
|
378
|
-
4. **One User** in **One Scope** from **One Provider** can have **Multiple Tokens**
|
|
379
|
-
5. **Email Uniqueness**: For the same provider, a user cannot have multiple tokens with the same email (validated via profile fetcher)
|
|
380
|
-
6. **Cross-Provider Emails**: The same email can exist across different providers
|
|
163
|
+
##### `getTokensByUserId(userId)`
|
|
381
164
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
The library provides automatic initialization of a default system and scope for simplified usage:
|
|
165
|
+
Get all tokens for a user.
|
|
385
166
|
|
|
386
167
|
```typescript
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
DEFAULT_SCOPE_NAME,
|
|
390
|
-
initializeDefaults,
|
|
391
|
-
} from '@dainprotocol/oauth2-token-manager';
|
|
392
|
-
|
|
393
|
-
// Automatic initialization
|
|
394
|
-
const oauth = new OAuth2Client({
|
|
395
|
-
providers: {
|
|
396
|
-
/* ... */
|
|
397
|
-
},
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
// If no system/scope is set, they will be automatically initialized on first use
|
|
401
|
-
const { url, state } = await oauth.authorize({ provider: 'google' });
|
|
402
|
-
|
|
403
|
-
// Or manually initialize defaults
|
|
404
|
-
await oauth.initializeDefaults();
|
|
405
|
-
|
|
406
|
-
// Check if defaults exist
|
|
407
|
-
const defaultSystem = await oauth.getDefaultSystem();
|
|
408
|
-
const defaultScope = await oauth.getDefaultScope();
|
|
409
|
-
|
|
410
|
-
// Default constants
|
|
411
|
-
console.log(DEFAULT_SYSTEM_NAME); // 'oauth2-token-manager-default-system'
|
|
412
|
-
console.log(DEFAULT_SCOPE_NAME); // 'oauth2-token-manager-default-scope'
|
|
168
|
+
const tokens = await oauth.getTokensByUserId('user123');
|
|
169
|
+
// Returns: StoredToken[]
|
|
413
170
|
```
|
|
414
171
|
|
|
415
|
-
|
|
172
|
+
##### `getTokensByEmail(email)`
|
|
416
173
|
|
|
417
|
-
|
|
418
|
-
- **Scope**: Type 'access', permissions ['read', 'write'], not isolated
|
|
174
|
+
Get all tokens for an email.
|
|
419
175
|
|
|
420
|
-
|
|
176
|
+
```typescript
|
|
177
|
+
const tokens = await oauth.getTokensByEmail('user@example.com');
|
|
178
|
+
// Returns: StoredToken[]
|
|
179
|
+
```
|
|
421
180
|
|
|
422
|
-
|
|
181
|
+
#### ๐๏ธ Token Cleanup
|
|
423
182
|
|
|
424
|
-
|
|
183
|
+
##### `deleteToken(provider, email)`
|
|
425
184
|
|
|
426
|
-
|
|
185
|
+
Delete a specific token.
|
|
427
186
|
|
|
428
187
|
```typescript
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
await oauth.getOrCreateSystem('MyApp'); // Idempotent - returns existing if found
|
|
432
|
-
await oauth.useSystem(systemId);
|
|
433
|
-
|
|
434
|
-
// Scope management
|
|
435
|
-
await oauth.createScope('api-access');
|
|
436
|
-
await oauth.getOrCreateScope('api-access'); // Idempotent - returns existing if found
|
|
437
|
-
|
|
438
|
-
// Default system/scope (automatic initialization)
|
|
439
|
-
await oauth.initializeDefaults(); // Creates default system and scope if they don't exist
|
|
440
|
-
const defaultSystem = await oauth.getDefaultSystem();
|
|
441
|
-
const defaultScope = await oauth.getDefaultScope();
|
|
442
|
-
|
|
443
|
-
// User management
|
|
444
|
-
const user = await oauth.getOrCreateUser({ email: 'user@example.com' });
|
|
445
|
-
await oauth.useUser(userId);
|
|
446
|
-
|
|
447
|
-
// Authorization flow
|
|
448
|
-
const { url, state } = await oauth.authorize({ provider: 'google' });
|
|
449
|
-
const result = await oauth.handleCallback(code, state);
|
|
450
|
-
|
|
451
|
-
// Token operations (uses current context + default scope)
|
|
452
|
-
// Note: When multiple tokens exist, these methods use the first (most recent) token
|
|
453
|
-
const accessToken = await oauth.getAccessToken('google');
|
|
454
|
-
const validToken = await oauth.ensureValidToken('google');
|
|
455
|
-
|
|
456
|
-
// Get all user tokens with auto-refresh (for current user)
|
|
457
|
-
const userTokens = await oauth.getUserTokens();
|
|
458
|
-
|
|
459
|
-
// Get all valid tokens for a specific user
|
|
460
|
-
const allTokens = await oauth.getAllValidTokensForUser(userId);
|
|
461
|
-
// Returns: { provider: string; scopeId: string; token: OAuth2Token; userToken: UserToken }[]
|
|
462
|
-
|
|
463
|
-
// Revoke tokens (uses current context)
|
|
464
|
-
await oauth.revokeTokens('google'); // Revokes for current user/scope/provider
|
|
188
|
+
const deleted = await oauth.deleteToken('google', 'user@example.com');
|
|
189
|
+
// Returns: boolean
|
|
465
190
|
```
|
|
466
191
|
|
|
467
|
-
|
|
192
|
+
##### `cleanupExpiredTokens()`
|
|
468
193
|
|
|
469
|
-
|
|
194
|
+
Delete all expired tokens.
|
|
470
195
|
|
|
471
196
|
```typescript
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
const storage = new InMemoryStorageAdapter();
|
|
475
|
-
const oauth = new OAuth2Client({ storage });
|
|
197
|
+
const count = await oauth.cleanupExpiredTokens();
|
|
198
|
+
// Returns: number of deleted tokens
|
|
476
199
|
```
|
|
477
200
|
|
|
478
|
-
|
|
201
|
+
##### `cleanupExpiredStates()`
|
|
479
202
|
|
|
480
|
-
|
|
481
|
-
import { PostgresStorageFactory } from '@dainprotocol/oauth2-storage-postgres';
|
|
482
|
-
|
|
483
|
-
const storage = await PostgresStorageFactory.create({
|
|
484
|
-
host: process.env.DB_HOST,
|
|
485
|
-
port: parseInt(process.env.DB_PORT || '5432'),
|
|
486
|
-
username: process.env.DB_USER,
|
|
487
|
-
password: process.env.DB_PASSWORD,
|
|
488
|
-
database: process.env.DB_NAME,
|
|
489
|
-
ssl: process.env.NODE_ENV === 'production',
|
|
490
|
-
});
|
|
491
|
-
```
|
|
492
|
-
|
|
493
|
-
#### Drizzle Adapter (Multi-Database)
|
|
203
|
+
Delete expired authorization states (older than 10 minutes).
|
|
494
204
|
|
|
495
205
|
```typescript
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
// PostgreSQL
|
|
499
|
-
const pgStorage = await DrizzleStorageFactory.create({
|
|
500
|
-
type: 'postgresql',
|
|
501
|
-
config: {
|
|
502
|
-
host: 'localhost',
|
|
503
|
-
port: 5432,
|
|
504
|
-
user: 'oauth2_user',
|
|
505
|
-
password: 'password',
|
|
506
|
-
database: 'oauth2_db',
|
|
507
|
-
},
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
// MySQL
|
|
511
|
-
const mysqlStorage = await DrizzleStorageFactory.create({
|
|
512
|
-
type: 'mysql',
|
|
513
|
-
config: {
|
|
514
|
-
host: 'localhost',
|
|
515
|
-
port: 3306,
|
|
516
|
-
user: 'oauth2_user',
|
|
517
|
-
password: 'password',
|
|
518
|
-
database: 'oauth2_db',
|
|
519
|
-
},
|
|
520
|
-
});
|
|
521
|
-
|
|
522
|
-
// SQLite
|
|
523
|
-
const sqliteStorage = await DrizzleStorageFactory.create({
|
|
524
|
-
type: 'sqlite',
|
|
525
|
-
config: {
|
|
526
|
-
filename: './oauth2.db',
|
|
527
|
-
},
|
|
528
|
-
});
|
|
206
|
+
const count = await oauth.cleanupExpiredStates();
|
|
207
|
+
// Returns: number of deleted states
|
|
529
208
|
```
|
|
530
209
|
|
|
531
|
-
####
|
|
210
|
+
#### โ๏ธ Provider Management
|
|
532
211
|
|
|
533
|
-
|
|
534
|
-
import { StorageAdapter } from '@dainprotocol/oauth2-token-manager';
|
|
212
|
+
##### `registerProvider(name, config)`
|
|
535
213
|
|
|
536
|
-
|
|
537
|
-
// System operations
|
|
538
|
-
async createSystem(system) {
|
|
539
|
-
/* implement */
|
|
540
|
-
}
|
|
541
|
-
async getOrCreateSystem(system) {
|
|
542
|
-
// Find by name, create if not exists
|
|
543
|
-
/* implement */
|
|
544
|
-
}
|
|
545
|
-
async getSystem(id) {
|
|
546
|
-
/* implement */
|
|
547
|
-
}
|
|
214
|
+
Register a new OAuth provider.
|
|
548
215
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
//
|
|
559
|
-
|
|
560
|
-
// Find by email or externalId, create if not exists
|
|
561
|
-
/* implement */
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
// ... implement all required methods
|
|
565
|
-
}
|
|
216
|
+
```typescript
|
|
217
|
+
oauth.registerProvider('custom', {
|
|
218
|
+
clientId: 'xxx',
|
|
219
|
+
clientSecret: 'xxx',
|
|
220
|
+
authorizationUrl: 'https://provider.com/oauth/authorize',
|
|
221
|
+
tokenUrl: 'https://provider.com/oauth/token',
|
|
222
|
+
redirectUri: 'http://localhost:3000/callback',
|
|
223
|
+
scopes: ['read'],
|
|
224
|
+
profileUrl?: 'https://provider.com/api/user', // Optional
|
|
225
|
+
usePKCE?: true, // Optional
|
|
226
|
+
});
|
|
566
227
|
```
|
|
567
228
|
|
|
568
|
-
###
|
|
229
|
+
### Types
|
|
569
230
|
|
|
570
|
-
####
|
|
231
|
+
#### OAuth2Config
|
|
571
232
|
|
|
572
233
|
```typescript
|
|
573
|
-
{
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
}
|
|
234
|
+
interface OAuth2Config {
|
|
235
|
+
clientId: string;
|
|
236
|
+
clientSecret?: string;
|
|
237
|
+
authorizationUrl: string;
|
|
238
|
+
tokenUrl: string;
|
|
239
|
+
redirectUri: string;
|
|
240
|
+
scopes: string[];
|
|
241
|
+
profileUrl?: string;
|
|
242
|
+
usePKCE?: boolean;
|
|
243
|
+
extraAuthParams?: Record<string, string>;
|
|
584
244
|
}
|
|
585
245
|
```
|
|
586
246
|
|
|
587
|
-
####
|
|
247
|
+
#### StoredToken
|
|
588
248
|
|
|
589
249
|
```typescript
|
|
590
|
-
{
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
}
|
|
250
|
+
interface StoredToken {
|
|
251
|
+
id: string;
|
|
252
|
+
provider: string;
|
|
253
|
+
userId: string;
|
|
254
|
+
email: string;
|
|
255
|
+
token: OAuth2Token;
|
|
256
|
+
metadata?: Record<string, any>;
|
|
257
|
+
createdAt: Date;
|
|
258
|
+
updatedAt: Date;
|
|
600
259
|
}
|
|
601
260
|
```
|
|
602
261
|
|
|
603
|
-
####
|
|
262
|
+
#### OAuth2Token
|
|
604
263
|
|
|
605
264
|
```typescript
|
|
606
|
-
{
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
redirectUri: 'http://localhost:3000/auth/callback',
|
|
613
|
-
scopes: ['read', 'write'],
|
|
614
|
-
profileUrl: 'https://provider.com/api/user',
|
|
615
|
-
additionalParams: {
|
|
616
|
-
audience: 'api.example.com'
|
|
617
|
-
},
|
|
618
|
-
responseRootKey: 'data' // For nested responses
|
|
619
|
-
}
|
|
265
|
+
interface OAuth2Token {
|
|
266
|
+
accessToken: string;
|
|
267
|
+
refreshToken?: string;
|
|
268
|
+
expiresAt: Date;
|
|
269
|
+
tokenType: string;
|
|
270
|
+
scope?: string;
|
|
620
271
|
}
|
|
621
272
|
```
|
|
622
273
|
|
|
623
|
-
##
|
|
274
|
+
## ๐ Storage Adapters
|
|
624
275
|
|
|
625
|
-
###
|
|
276
|
+
### In-Memory (Default)
|
|
626
277
|
|
|
627
278
|
```typescript
|
|
628
|
-
const
|
|
629
|
-
autoRefresh: true,
|
|
630
|
-
refreshBuffer: 10, // Refresh early to avoid expiry
|
|
631
|
-
expirationBuffer: 30, // Consider expired 30 seconds early
|
|
632
|
-
});
|
|
279
|
+
const oauth = new OAuth2Client(); // Uses in-memory storage
|
|
633
280
|
```
|
|
634
281
|
|
|
635
|
-
###
|
|
282
|
+
### PostgreSQL
|
|
636
283
|
|
|
637
284
|
```typescript
|
|
638
|
-
|
|
639
|
-
profileOptions: {
|
|
640
|
-
checkProfileEmail: true, // Fetch and check email conflicts
|
|
641
|
-
replaceConflictingTokens: true, // Replace existing tokens with same email
|
|
642
|
-
mergeUserData: true, // Merge profile data into user metadata
|
|
643
|
-
},
|
|
644
|
-
});
|
|
645
|
-
```
|
|
646
|
-
|
|
647
|
-
### PKCE Support
|
|
648
|
-
|
|
649
|
-
```typescript
|
|
650
|
-
// Enable PKCE for enhanced security
|
|
651
|
-
const { url, state } = await oauth.authorize({
|
|
652
|
-
provider: 'google',
|
|
653
|
-
usePKCE: true, // Enables PKCE flow
|
|
654
|
-
});
|
|
655
|
-
```
|
|
656
|
-
|
|
657
|
-
### Token Validation
|
|
658
|
-
|
|
659
|
-
```typescript
|
|
660
|
-
// Check if token is expired
|
|
661
|
-
const isExpired = oauth.isTokenExpired(token, {
|
|
662
|
-
expirationBuffer: 60, // Consider expired 60 seconds early
|
|
663
|
-
});
|
|
664
|
-
|
|
665
|
-
// Ensure valid token (auto-refresh if needed)
|
|
666
|
-
const validToken = await oauth.ensureValidToken('google');
|
|
667
|
-
```
|
|
668
|
-
|
|
669
|
-
## ๐ข Examples
|
|
285
|
+
import { PostgresStorageAdapter } from '@dainprotocol/oauth2-storage-postgres';
|
|
670
286
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
// Tenant-specific user management
|
|
678
|
-
await oauth.useSystem(tenantSystem.id);
|
|
679
|
-
const tenantUser = await oauth.getOrCreateUser({
|
|
680
|
-
email: userEmail,
|
|
681
|
-
metadata: { tenantId, role: 'admin' },
|
|
287
|
+
const storage = new PostgresStorageAdapter({
|
|
288
|
+
host: 'localhost',
|
|
289
|
+
port: 5432,
|
|
290
|
+
username: 'user',
|
|
291
|
+
password: 'password',
|
|
292
|
+
database: 'oauth_tokens',
|
|
682
293
|
});
|
|
683
294
|
|
|
684
|
-
|
|
685
|
-
const tokens = await oauth.granularV2.getTokens({ systemId: tenantSystem.id });
|
|
295
|
+
const oauth = new OAuth2Client({ storage });
|
|
686
296
|
```
|
|
687
297
|
|
|
688
|
-
###
|
|
298
|
+
### Drizzle ORM
|
|
689
299
|
|
|
690
300
|
```typescript
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
const analyticsSystem = await oauth.createSystem('Analytics Dashboard');
|
|
694
|
-
|
|
695
|
-
// Create scopes for different access levels
|
|
696
|
-
await oauth.useSystem(crmSystem.id);
|
|
697
|
-
const readScope = await oauth.createScope('read-only', {
|
|
698
|
-
type: 'access',
|
|
699
|
-
permissions: ['read:contacts', 'read:deals'],
|
|
700
|
-
isolated: true,
|
|
701
|
-
});
|
|
702
|
-
|
|
703
|
-
const adminScope = await oauth.createScope('admin', {
|
|
704
|
-
type: 'access',
|
|
705
|
-
permissions: ['*'],
|
|
706
|
-
isolated: true,
|
|
707
|
-
});
|
|
301
|
+
import { DrizzleStorageAdapter } from '@dainprotocol/oauth2-storage-drizzle';
|
|
302
|
+
import { drizzle } from 'drizzle-orm/postgres-js';
|
|
708
303
|
|
|
709
|
-
|
|
710
|
-
const
|
|
711
|
-
|
|
712
|
-
// Authorize for specific system/scope
|
|
713
|
-
const { url } = await oauth.authorize({
|
|
714
|
-
provider: 'google',
|
|
715
|
-
scopes: ['profile', 'email'],
|
|
716
|
-
});
|
|
304
|
+
const db = drizzle(/* your db config */);
|
|
305
|
+
const storage = new DrizzleStorageAdapter(db, { dialect: 'postgres' });
|
|
306
|
+
const oauth = new OAuth2Client({ storage });
|
|
717
307
|
```
|
|
718
308
|
|
|
719
|
-
##
|
|
309
|
+
## ๐ Examples
|
|
720
310
|
|
|
721
|
-
###
|
|
311
|
+
### Express.js Integration
|
|
722
312
|
|
|
723
313
|
```typescript
|
|
314
|
+
import express from 'express';
|
|
315
|
+
import { OAuth2Client } from '@dainprotocol/oauth2-token-manager';
|
|
316
|
+
|
|
317
|
+
const app = express();
|
|
724
318
|
const oauth = new OAuth2Client({
|
|
725
|
-
storage: await PostgresStorageFactory.create({
|
|
726
|
-
host: process.env.DB_HOST,
|
|
727
|
-
port: parseInt(process.env.DB_PORT || '5432'),
|
|
728
|
-
username: process.env.DB_USER,
|
|
729
|
-
password: process.env.DB_PASSWORD,
|
|
730
|
-
database: process.env.DB_NAME,
|
|
731
|
-
ssl: {
|
|
732
|
-
rejectUnauthorized: process.env.NODE_ENV === 'production',
|
|
733
|
-
},
|
|
734
|
-
poolSize: 20,
|
|
735
|
-
logging: process.env.NODE_ENV === 'development',
|
|
736
|
-
}),
|
|
737
|
-
sealKey: process.env.OAUTH2_SEAL_KEY, // For token encryption
|
|
738
319
|
providers: {
|
|
739
320
|
google: {
|
|
740
321
|
clientId: process.env.GOOGLE_CLIENT_ID,
|
|
741
322
|
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
742
|
-
|
|
743
|
-
|
|
323
|
+
authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
|
|
324
|
+
tokenUrl: 'https://oauth2.googleapis.com/token',
|
|
325
|
+
redirectUri: 'http://localhost:3000/auth/google/callback',
|
|
326
|
+
scopes: ['profile', 'email'],
|
|
744
327
|
},
|
|
745
328
|
},
|
|
746
329
|
});
|
|
747
|
-
```
|
|
748
330
|
|
|
749
|
-
|
|
331
|
+
// Start OAuth flow
|
|
332
|
+
app.get('/auth/:provider', async (req, res) => {
|
|
333
|
+
const { url, state } = await oauth.authorize({
|
|
334
|
+
provider: req.params.provider,
|
|
335
|
+
userId: req.user.id,
|
|
336
|
+
email: req.user.email,
|
|
337
|
+
});
|
|
750
338
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
const accessToken = await oauth.getAccessToken('google', {
|
|
754
|
-
autoRefresh: true,
|
|
755
|
-
refreshBuffer: 10, // Refresh early to avoid expiry
|
|
339
|
+
req.session.oauthState = state;
|
|
340
|
+
res.redirect(url);
|
|
756
341
|
});
|
|
757
342
|
|
|
758
|
-
//
|
|
759
|
-
|
|
343
|
+
// Handle callback
|
|
344
|
+
app.get('/auth/:provider/callback', async (req, res) => {
|
|
345
|
+
const { code } = req.query;
|
|
346
|
+
const { token, profile } = await oauth.handleCallback(code, req.session.oauthState);
|
|
347
|
+
res.json({ success: true, profile });
|
|
348
|
+
});
|
|
760
349
|
|
|
761
|
-
//
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
5 * 60 * 1000,
|
|
767
|
-
); // Every 5 minutes
|
|
350
|
+
// Use tokens
|
|
351
|
+
app.get('/api/data', async (req, res) => {
|
|
352
|
+
const accessToken = await oauth.getAccessToken('google', req.user.email);
|
|
353
|
+
// Use accessToken for API calls...
|
|
354
|
+
});
|
|
768
355
|
```
|
|
769
356
|
|
|
770
|
-
###
|
|
357
|
+
### Scheduled Cleanup
|
|
771
358
|
|
|
772
359
|
```typescript
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
```
|
|
785
|
-
|
|
786
|
-
## ๐ Security Features
|
|
787
|
-
|
|
788
|
-
### State Management
|
|
789
|
-
|
|
790
|
-
- Cryptographically secure state generation
|
|
791
|
-
- Automatic state validation and cleanup
|
|
792
|
-
- Configurable state expiration
|
|
793
|
-
|
|
794
|
-
### PKCE (Proof Key for Code Exchange)
|
|
795
|
-
|
|
796
|
-
- Built-in PKCE support for public clients
|
|
797
|
-
- Automatic code verifier generation
|
|
798
|
-
- Enhanced security for mobile and SPA applications
|
|
799
|
-
|
|
800
|
-
### Token Encryption
|
|
801
|
-
|
|
802
|
-
- Secure token storage with optional encryption
|
|
803
|
-
- Configurable seal keys for sensitive data
|
|
804
|
-
- Protection against token theft
|
|
805
|
-
|
|
806
|
-
### Email Validation
|
|
807
|
-
|
|
808
|
-
- Automatic email conflict detection
|
|
809
|
-
- Profile-based user validation
|
|
810
|
-
- Cross-provider email consistency
|
|
811
|
-
|
|
812
|
-
## ๐งช Testing
|
|
813
|
-
|
|
814
|
-
The library includes comprehensive tests using Vitest:
|
|
815
|
-
|
|
816
|
-
```bash
|
|
817
|
-
# Run tests
|
|
818
|
-
npm test
|
|
819
|
-
|
|
820
|
-
# Run tests with UI
|
|
821
|
-
npm run test:ui
|
|
822
|
-
|
|
823
|
-
# Run tests with coverage
|
|
824
|
-
npm run test:coverage
|
|
825
|
-
|
|
826
|
-
# Watch mode
|
|
827
|
-
npm run test:watch
|
|
360
|
+
// Clean up expired tokens daily
|
|
361
|
+
setInterval(
|
|
362
|
+
async () => {
|
|
363
|
+
const tokens = await oauth.cleanupExpiredTokens();
|
|
364
|
+
const states = await oauth.cleanupExpiredStates();
|
|
365
|
+
console.log(`Cleaned up ${tokens} tokens and ${states} states`);
|
|
366
|
+
},
|
|
367
|
+
24 * 60 * 60 * 1000,
|
|
368
|
+
);
|
|
828
369
|
```
|
|
829
370
|
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
1. Fork the repository
|
|
833
|
-
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
834
|
-
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
835
|
-
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
836
|
-
5. Open a Pull Request
|
|
837
|
-
|
|
838
|
-
### Development Setup
|
|
839
|
-
|
|
840
|
-
```bash
|
|
841
|
-
# Install dependencies
|
|
842
|
-
npm install
|
|
843
|
-
|
|
844
|
-
# Run in development mode
|
|
845
|
-
npm run dev
|
|
371
|
+
### Multiple Providers
|
|
846
372
|
|
|
847
|
-
|
|
848
|
-
|
|
373
|
+
```typescript
|
|
374
|
+
// User can connect multiple OAuth accounts
|
|
375
|
+
const providers = ['google', 'github', 'microsoft'];
|
|
849
376
|
|
|
850
|
-
|
|
851
|
-
|
|
377
|
+
for (const provider of providers) {
|
|
378
|
+
const { url, state } = await oauth.authorize({
|
|
379
|
+
provider,
|
|
380
|
+
userId: 'user123',
|
|
381
|
+
email: 'user@example.com',
|
|
382
|
+
});
|
|
383
|
+
// Handle each provider...
|
|
384
|
+
}
|
|
852
385
|
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
386
|
+
// Get all connected accounts
|
|
387
|
+
const tokens = await oauth.getTokensByUserId('user123');
|
|
388
|
+
console.log(
|
|
389
|
+
'Connected accounts:',
|
|
390
|
+
tokens.map((t) => t.provider),
|
|
391
|
+
);
|
|
856
392
|
```
|
|
857
393
|
|
|
858
394
|
## ๐ License
|
|
859
395
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
## ๐โโ๏ธ Support
|
|
863
|
-
|
|
864
|
-
- ๐ [Documentation](https://github.com/dainprotocol/oauth2-token-manager#readme)
|
|
865
|
-
- ๐ [Issue Tracker](https://github.com/dainprotocol/oauth2-token-manager/issues)
|
|
866
|
-
- ๐ฌ [Discussions](https://github.com/dainprotocol/oauth2-token-manager/discussions)
|
|
396
|
+
MIT ยฉ Dain Protocol
|