@dainprotocol/oauth2-token-manager 0.1.0 โ 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 +237 -627
- package/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +119 -579
- package/dist/index.d.ts +119 -579
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +28 -31
package/README.md
CHANGED
|
@@ -1,19 +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
|
+
|
|
5
|
+
## ๐ Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Features](#-features)
|
|
8
|
+
- [Installation](#-installation)
|
|
9
|
+
- [Quick Start](#-quick-start)
|
|
10
|
+
- [Core Concepts](#-core-concepts)
|
|
11
|
+
- [API Reference](#-api-reference)
|
|
12
|
+
- [Storage Adapters](#-storage-adapters)
|
|
13
|
+
- [Examples](#-examples)
|
|
4
14
|
|
|
5
15
|
## ๐ Features
|
|
6
16
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
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
|
+
- **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
|
|
17
23
|
|
|
18
24
|
## ๐ฆ Installation
|
|
19
25
|
|
|
@@ -24,763 +30,367 @@ npm install @dainprotocol/oauth2-token-manager
|
|
|
24
30
|
### Storage Adapters
|
|
25
31
|
|
|
26
32
|
```bash
|
|
27
|
-
# PostgreSQL adapter
|
|
33
|
+
# PostgreSQL adapter (TypeORM based)
|
|
28
34
|
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
35
|
|
|
38
|
-
|
|
39
|
-
|
|
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);
|
|
36
|
+
# Drizzle adapter (supports PostgreSQL, MySQL, SQLite)
|
|
37
|
+
npm install @dainprotocol/oauth2-storage-drizzle
|
|
73
38
|
```
|
|
74
39
|
|
|
75
|
-
|
|
40
|
+
## ๐ Quick Start
|
|
76
41
|
|
|
77
42
|
```typescript
|
|
78
43
|
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
44
|
|
|
45
|
+
// Initialize with provider configurations
|
|
90
46
|
const oauth = new OAuth2Client({
|
|
91
|
-
storage,
|
|
92
47
|
providers: {
|
|
93
48
|
google: {
|
|
94
|
-
|
|
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',
|
|
95
56
|
},
|
|
96
57
|
github: {
|
|
97
|
-
|
|
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',
|
|
98
65
|
},
|
|
99
66
|
},
|
|
100
67
|
});
|
|
101
68
|
|
|
102
|
-
//
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
69
|
+
// Start OAuth flow
|
|
70
|
+
const { url, state } = await oauth.authorize({
|
|
71
|
+
provider: 'google',
|
|
72
|
+
userId: 'user123',
|
|
73
|
+
email: 'user@example.com',
|
|
74
|
+
scopes: ['profile', 'email'],
|
|
108
75
|
});
|
|
109
|
-
```
|
|
110
76
|
|
|
111
|
-
|
|
77
|
+
// Redirect user to authorization URL
|
|
78
|
+
res.redirect(url);
|
|
112
79
|
|
|
113
|
-
|
|
80
|
+
// Handle OAuth callback
|
|
81
|
+
const { token, profile } = await oauth.handleCallback(code, state);
|
|
114
82
|
|
|
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
|
-
โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ
|
|
83
|
+
// Use the token
|
|
84
|
+
const accessToken = await oauth.getAccessToken('google', 'user@example.com');
|
|
130
85
|
```
|
|
131
86
|
|
|
132
|
-
|
|
87
|
+
## ๐ Core Concepts
|
|
133
88
|
|
|
134
|
-
|
|
89
|
+
### How It Works
|
|
135
90
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
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)
|
|
183
95
|
|
|
184
96
|
## ๐ API Reference
|
|
185
97
|
|
|
186
98
|
### OAuth2Client
|
|
187
99
|
|
|
188
|
-
The main
|
|
189
|
-
|
|
190
|
-
#### Context-Managed API (Recommended)
|
|
100
|
+
The main class for managing OAuth2 tokens.
|
|
191
101
|
|
|
192
102
|
```typescript
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
103
|
+
const oauth = new OAuth2Client({
|
|
104
|
+
storage?: StorageAdapter, // Optional, defaults to in-memory
|
|
105
|
+
providers?: { // OAuth provider configurations
|
|
106
|
+
[name: string]: OAuth2Config
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
```
|
|
200
110
|
|
|
201
|
-
|
|
202
|
-
const { url, state } = await oauth.authorize({ provider: 'google' });
|
|
203
|
-
const result = await oauth.handleCallback(code, state);
|
|
111
|
+
### Methods
|
|
204
112
|
|
|
205
|
-
|
|
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');
|
|
113
|
+
#### ๐ OAuth Flow
|
|
209
114
|
|
|
210
|
-
|
|
211
|
-
const userTokens = await oauth.getUserTokens();
|
|
115
|
+
##### `authorize(options)`
|
|
212
116
|
|
|
213
|
-
|
|
214
|
-
const allTokens = await oauth.getAllValidTokensForUser(userId);
|
|
215
|
-
// Returns: { provider: string; scopeId: string; token: OAuth2Token; userToken: UserToken }[]
|
|
117
|
+
Start OAuth2 authorization flow.
|
|
216
118
|
|
|
217
|
-
|
|
218
|
-
|
|
119
|
+
```typescript
|
|
120
|
+
const { url, state } = await oauth.authorize({
|
|
121
|
+
provider: 'google',
|
|
122
|
+
userId: 'user123',
|
|
123
|
+
email: 'user@example.com',
|
|
124
|
+
scopes?: ['profile', 'email'], // Optional
|
|
125
|
+
usePKCE?: true, // Optional
|
|
126
|
+
metadata?: { source: 'signup' } // Optional
|
|
127
|
+
});
|
|
128
|
+
// Redirect user to `url`
|
|
219
129
|
```
|
|
220
130
|
|
|
221
|
-
|
|
131
|
+
##### `handleCallback(code, state)`
|
|
222
132
|
|
|
223
|
-
|
|
133
|
+
Handle OAuth2 callback and save tokens.
|
|
224
134
|
|
|
225
135
|
```typescript
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
//
|
|
229
|
-
|
|
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 ===
|
|
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
|
|
139
|
+
```
|
|
279
140
|
|
|
280
|
-
|
|
281
|
-
const validToken = await oauth.granular.getValidTokenForUser(userId, scopeId, 'google');
|
|
141
|
+
#### ๐ Token Management
|
|
282
142
|
|
|
283
|
-
|
|
284
|
-
const accessToken = await oauth.granular.getAccessTokenForUser(userId, scopeId, 'google');
|
|
143
|
+
##### `getAccessToken(provider, email, options?)`
|
|
285
144
|
|
|
286
|
-
|
|
287
|
-
const savedToken = await oauth.granular.saveTokenForUser(
|
|
288
|
-
userId,
|
|
289
|
-
systemId,
|
|
290
|
-
scopeId,
|
|
291
|
-
'google',
|
|
292
|
-
'user@example.com',
|
|
293
|
-
oauthToken,
|
|
294
|
-
);
|
|
145
|
+
Get access token string (auto-refreshes if expired).
|
|
295
146
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
//
|
|
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
|
|
147
|
+
```typescript
|
|
148
|
+
const accessToken = await oauth.getAccessToken('google', 'user@example.com');
|
|
149
|
+
// Returns: "ya29.a0AfH6SMBx..."
|
|
302
150
|
```
|
|
303
151
|
|
|
304
|
-
|
|
152
|
+
##### `getValidToken(provider, email, options?)`
|
|
305
153
|
|
|
306
|
-
|
|
154
|
+
Get full token object (auto-refreshes if expired).
|
|
307
155
|
|
|
308
156
|
```typescript
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const storage = new InMemoryStorageAdapter();
|
|
312
|
-
const oauth = new OAuth2Client({ storage });
|
|
157
|
+
const token = await oauth.getValidToken('google', 'user@example.com');
|
|
158
|
+
// Returns: { accessToken, refreshToken, expiresAt, tokenType, scope, ... }
|
|
313
159
|
```
|
|
314
160
|
|
|
315
|
-
####
|
|
161
|
+
#### ๐ Token Queries
|
|
316
162
|
|
|
317
|
-
|
|
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
|
-
```
|
|
163
|
+
##### `getTokensByUserId(userId)`
|
|
329
164
|
|
|
330
|
-
|
|
165
|
+
Get all tokens for a user.
|
|
331
166
|
|
|
332
167
|
```typescript
|
|
333
|
-
|
|
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
|
-
}
|
|
168
|
+
const tokens = await oauth.getTokensByUserId('user123');
|
|
169
|
+
// Returns: StoredToken[]
|
|
344
170
|
```
|
|
345
171
|
|
|
346
|
-
|
|
172
|
+
##### `getTokensByEmail(email)`
|
|
347
173
|
|
|
348
|
-
|
|
174
|
+
Get all tokens for an email.
|
|
349
175
|
|
|
350
176
|
```typescript
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
}
|
|
177
|
+
const tokens = await oauth.getTokensByEmail('user@example.com');
|
|
178
|
+
// Returns: StoredToken[]
|
|
379
179
|
```
|
|
380
180
|
|
|
381
|
-
####
|
|
181
|
+
#### ๐๏ธ Token Cleanup
|
|
382
182
|
|
|
383
|
-
|
|
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
|
-
```
|
|
183
|
+
##### `deleteToken(provider, email)`
|
|
400
184
|
|
|
401
|
-
|
|
185
|
+
Delete a specific token.
|
|
402
186
|
|
|
403
187
|
```typescript
|
|
404
|
-
const
|
|
405
|
-
|
|
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
|
-
});
|
|
188
|
+
const deleted = await oauth.deleteToken('google', 'user@example.com');
|
|
189
|
+
// Returns: boolean
|
|
426
190
|
```
|
|
427
191
|
|
|
428
|
-
|
|
192
|
+
##### `cleanupExpiredTokens()`
|
|
429
193
|
|
|
430
|
-
|
|
194
|
+
Delete all expired tokens.
|
|
431
195
|
|
|
432
196
|
```typescript
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
}
|
|
197
|
+
const count = await oauth.cleanupExpiredTokens();
|
|
198
|
+
// Returns: number of deleted tokens
|
|
450
199
|
```
|
|
451
200
|
|
|
452
|
-
|
|
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
|
|
201
|
+
##### `cleanupExpiredStates()`
|
|
460
202
|
|
|
461
|
-
|
|
203
|
+
Delete expired authorization states (older than 10 minutes).
|
|
462
204
|
|
|
463
205
|
```typescript
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
refreshBuffer: 5, // Refresh 5 minutes before expiry
|
|
467
|
-
expirationBuffer: 30, // Consider expired 30 seconds early
|
|
468
|
-
});
|
|
206
|
+
const count = await oauth.cleanupExpiredStates();
|
|
207
|
+
// Returns: number of deleted states
|
|
469
208
|
```
|
|
470
209
|
|
|
471
|
-
|
|
210
|
+
#### โ๏ธ Provider Management
|
|
472
211
|
|
|
473
|
-
|
|
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
|
-
```
|
|
212
|
+
##### `registerProvider(name, config)`
|
|
482
213
|
|
|
483
|
-
|
|
214
|
+
Register a new OAuth provider.
|
|
484
215
|
|
|
485
216
|
```typescript
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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');
|
|
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
|
+
});
|
|
528
227
|
```
|
|
529
228
|
|
|
530
|
-
###
|
|
229
|
+
### Types
|
|
531
230
|
|
|
532
|
-
|
|
231
|
+
#### OAuth2Config
|
|
533
232
|
|
|
534
233
|
```typescript
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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');
|
|
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>;
|
|
244
|
+
}
|
|
561
245
|
```
|
|
562
246
|
|
|
563
|
-
|
|
247
|
+
#### StoredToken
|
|
564
248
|
|
|
565
249
|
```typescript
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
provider:
|
|
569
|
-
|
|
570
|
-
|
|
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;
|
|
259
|
+
}
|
|
571
260
|
```
|
|
572
261
|
|
|
573
|
-
|
|
262
|
+
#### OAuth2Token
|
|
574
263
|
|
|
575
264
|
```typescript
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
265
|
+
interface OAuth2Token {
|
|
266
|
+
accessToken: string;
|
|
267
|
+
refreshToken?: string;
|
|
268
|
+
expiresAt: Date;
|
|
269
|
+
tokenType: string;
|
|
270
|
+
scope?: string;
|
|
271
|
+
}
|
|
583
272
|
```
|
|
584
273
|
|
|
585
|
-
##
|
|
586
|
-
|
|
587
|
-
### State Management
|
|
274
|
+
## ๐ Storage Adapters
|
|
588
275
|
|
|
589
|
-
-
|
|
590
|
-
- Automatic state validation and cleanup
|
|
591
|
-
- Configurable state expiration
|
|
276
|
+
### In-Memory (Default)
|
|
592
277
|
|
|
593
|
-
|
|
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
|
|
278
|
+
```typescript
|
|
279
|
+
const oauth = new OAuth2Client(); // Uses in-memory storage
|
|
627
280
|
```
|
|
628
281
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
### SaaS Platform with Multiple Apps
|
|
282
|
+
### PostgreSQL
|
|
632
283
|
|
|
633
284
|
```typescript
|
|
634
|
-
|
|
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
|
-
});
|
|
285
|
+
import { PostgresStorageAdapter } from '@dainprotocol/oauth2-storage-postgres';
|
|
645
286
|
|
|
646
|
-
const
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
287
|
+
const storage = new PostgresStorageAdapter({
|
|
288
|
+
host: 'localhost',
|
|
289
|
+
port: 5432,
|
|
290
|
+
username: 'user',
|
|
291
|
+
password: 'password',
|
|
292
|
+
database: 'oauth_tokens',
|
|
650
293
|
});
|
|
651
294
|
|
|
652
|
-
|
|
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
|
-
});
|
|
295
|
+
const oauth = new OAuth2Client({ storage });
|
|
660
296
|
```
|
|
661
297
|
|
|
662
|
-
###
|
|
298
|
+
### Drizzle ORM
|
|
663
299
|
|
|
664
300
|
```typescript
|
|
665
|
-
|
|
666
|
-
|
|
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
|
-
});
|
|
301
|
+
import { DrizzleStorageAdapter } from '@dainprotocol/oauth2-storage-drizzle';
|
|
302
|
+
import { drizzle } from 'drizzle-orm/postgres-js';
|
|
674
303
|
|
|
675
|
-
|
|
676
|
-
const
|
|
304
|
+
const db = drizzle(/* your db config */);
|
|
305
|
+
const storage = new DrizzleStorageAdapter(db, { dialect: 'postgres' });
|
|
306
|
+
const oauth = new OAuth2Client({ storage });
|
|
677
307
|
```
|
|
678
308
|
|
|
679
|
-
##
|
|
309
|
+
## ๐ Examples
|
|
680
310
|
|
|
681
|
-
###
|
|
311
|
+
### Express.js Integration
|
|
682
312
|
|
|
683
313
|
```typescript
|
|
314
|
+
import express from 'express';
|
|
315
|
+
import { OAuth2Client } from '@dainprotocol/oauth2-token-manager';
|
|
316
|
+
|
|
317
|
+
const app = express();
|
|
684
318
|
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
319
|
providers: {
|
|
699
320
|
google: {
|
|
700
321
|
clientId: process.env.GOOGLE_CLIENT_ID,
|
|
701
322
|
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
702
|
-
|
|
703
|
-
|
|
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'],
|
|
704
327
|
},
|
|
705
328
|
},
|
|
706
329
|
});
|
|
707
|
-
```
|
|
708
330
|
|
|
709
|
-
|
|
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
|
+
});
|
|
338
|
+
|
|
339
|
+
req.session.oauthState = state;
|
|
340
|
+
res.redirect(url);
|
|
341
|
+
});
|
|
710
342
|
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
const
|
|
714
|
-
|
|
715
|
-
|
|
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 });
|
|
716
348
|
});
|
|
717
349
|
|
|
718
|
-
//
|
|
719
|
-
|
|
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
|
+
});
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### Scheduled Cleanup
|
|
720
358
|
|
|
721
|
-
|
|
359
|
+
```typescript
|
|
360
|
+
// Clean up expired tokens daily
|
|
722
361
|
setInterval(
|
|
723
362
|
async () => {
|
|
724
|
-
await oauth.
|
|
363
|
+
const tokens = await oauth.cleanupExpiredTokens();
|
|
364
|
+
const states = await oauth.cleanupExpiredStates();
|
|
365
|
+
console.log(`Cleaned up ${tokens} tokens and ${states} states`);
|
|
725
366
|
},
|
|
726
|
-
|
|
727
|
-
);
|
|
367
|
+
24 * 60 * 60 * 1000,
|
|
368
|
+
);
|
|
728
369
|
```
|
|
729
370
|
|
|
730
|
-
###
|
|
371
|
+
### Multiple Providers
|
|
731
372
|
|
|
732
373
|
```typescript
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
374
|
+
// User can connect multiple OAuth accounts
|
|
375
|
+
const providers = ['google', 'github', 'microsoft'];
|
|
376
|
+
|
|
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...
|
|
743
384
|
}
|
|
744
|
-
```
|
|
745
385
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
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
|
|
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
|
+
);
|
|
772
392
|
```
|
|
773
393
|
|
|
774
394
|
## ๐ License
|
|
775
395
|
|
|
776
|
-
|
|
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)
|
|
396
|
+
MIT ยฉ Dain Protocol
|