@vunexa/lixa 0.0.1-alpha.19 → 0.0.1-alpha.21
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 +717 -29
- package/dist/dao/types.d.ts +239 -5
- package/dist/dao/types.d.ts.map +1 -1
- package/dist/export-types/index.d.ts +785 -43
- package/dist/index.cjs +277 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +787 -40
- package/dist/index.d.ts +11 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +277 -41
- package/dist/index.js.map +1 -1
- package/dist/lixa.d.ts +185 -16
- package/dist/lixa.d.ts.map +1 -1
- package/dist/models/session.d.ts +107 -4
- package/dist/models/session.d.ts.map +1 -1
- package/dist/providers/IProvider.d.ts +121 -4
- package/dist/providers/IProvider.d.ts.map +1 -1
- package/dist/providers/index.d.ts +0 -2
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/types.d.ts +129 -14
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -16
- package/dist/IProvider-2tDwRAnH.d.cts +0 -18
- package/dist/IProvider-2tDwRAnH.d.ts +0 -18
- package/dist/export-types/providers.d.ts +0 -61
- package/dist/providers/github.d.ts +0 -13
- package/dist/providers/github.d.ts.map +0 -1
- package/dist/providers/google.d.ts +0 -13
- package/dist/providers/google.d.ts.map +0 -1
- package/dist/providers-entry.cjs +0 -48
- package/dist/providers-entry.cjs.map +0 -1
- package/dist/providers-entry.d.cts +0 -25
- package/dist/providers-entry.d.ts +0 -22
- package/dist/providers-entry.d.ts.map +0 -1
- package/dist/providers-entry.js +0 -20
- package/dist/providers-entry.js.map +0 -1
package/README.md
CHANGED
|
@@ -33,14 +33,22 @@ yarn add @vunexa/lixa
|
|
|
33
33
|
|
|
34
34
|
## Quick Start
|
|
35
35
|
|
|
36
|
-
### 1.
|
|
36
|
+
### 1. Install packages
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install @vunexa/lixa @vunexa/lixa-providers
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 2. Configure Lixa with providers
|
|
37
43
|
|
|
38
44
|
```typescript
|
|
39
45
|
import { Lixa } from "@vunexa/lixa";
|
|
46
|
+
import { GoogleProvider, GithubProvider } from "@vunexa/lixa-providers";
|
|
40
47
|
|
|
41
48
|
const lixa = new Lixa({
|
|
42
49
|
providers: {
|
|
43
50
|
google: {
|
|
51
|
+
provider: new GoogleProvider(),
|
|
44
52
|
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
45
53
|
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
46
54
|
redirectUri: "https://yourapp.com/auth/google/callback",
|
|
@@ -51,11 +59,11 @@ const lixa = new Lixa({
|
|
|
51
59
|
},
|
|
52
60
|
},
|
|
53
61
|
github: {
|
|
62
|
+
provider: new GithubProvider(),
|
|
54
63
|
clientId: process.env.GITHUB_CLIENT_ID!,
|
|
55
64
|
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
56
65
|
redirectUri: "https://yourapp.com/auth/github/callback",
|
|
57
66
|
scopes: ["read:user", "user:email"],
|
|
58
|
-
extraConfig: {},
|
|
59
67
|
},
|
|
60
68
|
},
|
|
61
69
|
|
|
@@ -72,22 +80,22 @@ const lixa = new Lixa({
|
|
|
72
80
|
});
|
|
73
81
|
```
|
|
74
82
|
|
|
75
|
-
###
|
|
83
|
+
### 3. Redirect users to the provider's authorization URL
|
|
76
84
|
|
|
77
85
|
```typescript
|
|
78
86
|
app.get("/login", (req, res) => {
|
|
79
87
|
const provider = req.query.provider as string; // 'google' or 'github'
|
|
80
|
-
const state =
|
|
88
|
+
const state = Lixa.generateRandomState();
|
|
81
89
|
|
|
82
90
|
// Store state in session for validation
|
|
83
91
|
req.session.oauthState = state;
|
|
84
92
|
|
|
85
|
-
const authUrl = lixa.getAuthUrl(provider
|
|
93
|
+
const authUrl = lixa.getAuthUrl(provider, state);
|
|
86
94
|
res.redirect(authUrl);
|
|
87
95
|
});
|
|
88
96
|
```
|
|
89
97
|
|
|
90
|
-
###
|
|
98
|
+
### 4. Handle the provider callback and establish a session
|
|
91
99
|
|
|
92
100
|
```typescript
|
|
93
101
|
app.get("/auth/:provider/callback", async (req, res) => {
|
|
@@ -100,14 +108,17 @@ app.get("/auth/:provider/callback", async (req, res) => {
|
|
|
100
108
|
throw new Error("Invalid state parameter");
|
|
101
109
|
}
|
|
102
110
|
|
|
103
|
-
const
|
|
111
|
+
const sessionId = await lixa.handleCallback({
|
|
104
112
|
provider,
|
|
105
113
|
code: code as string,
|
|
106
114
|
state: state as string,
|
|
107
115
|
});
|
|
108
116
|
|
|
109
|
-
// Session established
|
|
110
|
-
|
|
117
|
+
// Session established - retrieve session info if needed
|
|
118
|
+
const session = await lixa.fetchSessionInfo(sessionId);
|
|
119
|
+
|
|
120
|
+
// Store session ID in cookie
|
|
121
|
+
res.cookie("session_id", sessionId, {
|
|
111
122
|
httpOnly: true,
|
|
112
123
|
secure: true,
|
|
113
124
|
sameSite: "strict",
|
|
@@ -121,11 +132,277 @@ app.get("/auth/:provider/callback", async (req, res) => {
|
|
|
121
132
|
});
|
|
122
133
|
```
|
|
123
134
|
|
|
135
|
+
## Provider Configuration
|
|
136
|
+
|
|
137
|
+
### Using Built-in Providers
|
|
138
|
+
|
|
139
|
+
Built-in providers are available in the `@vunexa/lixa-providers` package:
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
import { Lixa } from "@vunexa/lixa";
|
|
143
|
+
import { GoogleProvider, GithubProvider } from "@vunexa/lixa-providers";
|
|
144
|
+
|
|
145
|
+
const lixa = new Lixa({
|
|
146
|
+
providers: {
|
|
147
|
+
google: {
|
|
148
|
+
provider: new GoogleProvider(),
|
|
149
|
+
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
150
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
151
|
+
redirectUri: "https://yourapp.com/auth/google/callback",
|
|
152
|
+
scopes: ["openid", "email", "profile"]
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Using Custom Inline Providers
|
|
159
|
+
|
|
160
|
+
You can create custom providers by implementing the `IProvider` interface:
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
import { Lixa, IProvider } from "@vunexa/lixa";
|
|
164
|
+
|
|
165
|
+
// Define your custom provider
|
|
166
|
+
const customProvider: IProvider = {
|
|
167
|
+
authorizationEndpoint: "https://provider.com/oauth/authorize",
|
|
168
|
+
tokenEndpoint: "https://provider.com/oauth/token",
|
|
169
|
+
userInfoEndpoint: "https://provider.com/api/user"
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// Use it directly in configuration
|
|
173
|
+
const lixa = new Lixa({
|
|
174
|
+
providers: {
|
|
175
|
+
custom: {
|
|
176
|
+
provider: customProvider,
|
|
177
|
+
clientId: "your-client-id",
|
|
178
|
+
clientSecret: "your-client-secret",
|
|
179
|
+
redirectUri: "https://yourapp.com/auth/custom/callback",
|
|
180
|
+
scopes: ["read:user"]
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Mixing Built-in and Custom Providers
|
|
187
|
+
|
|
188
|
+
You can use both built-in and custom providers in the same configuration:
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
import { Lixa, IProvider } from "@vunexa/lixa";
|
|
192
|
+
import { GoogleProvider } from "@vunexa/lixa-providers";
|
|
193
|
+
|
|
194
|
+
const customProvider: IProvider = {
|
|
195
|
+
authorizationEndpoint: "https://custom.com/oauth/authorize",
|
|
196
|
+
tokenEndpoint: "https://custom.com/oauth/token",
|
|
197
|
+
userInfoEndpoint: "https://custom.com/api/user"
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const lixa = new Lixa({
|
|
201
|
+
providers: {
|
|
202
|
+
google: {
|
|
203
|
+
provider: new GoogleProvider(),
|
|
204
|
+
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
205
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
206
|
+
redirectUri: "https://yourapp.com/auth/google/callback",
|
|
207
|
+
scopes: ["openid", "email", "profile"]
|
|
208
|
+
},
|
|
209
|
+
custom: {
|
|
210
|
+
provider: customProvider,
|
|
211
|
+
clientId: process.env.CUSTOM_CLIENT_ID!,
|
|
212
|
+
clientSecret: process.env.CUSTOM_CLIENT_SECRET!,
|
|
213
|
+
redirectUri: "https://yourapp.com/auth/custom/callback",
|
|
214
|
+
scopes: ["read:user"]
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Overriding Built-in Providers (Testing)
|
|
221
|
+
|
|
222
|
+
You can override built-in providers with custom implementations for testing:
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
import { Lixa, IProvider } from "@vunexa/lixa";
|
|
226
|
+
|
|
227
|
+
// Mock provider for testing
|
|
228
|
+
const mockGoogleProvider: IProvider = {
|
|
229
|
+
authorizationEndpoint: "http://localhost:3000/mock/authorize",
|
|
230
|
+
tokenEndpoint: "http://localhost:3000/mock/token",
|
|
231
|
+
userInfoEndpoint: "http://localhost:3000/mock/userinfo"
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const lixa = new Lixa({
|
|
235
|
+
providers: {
|
|
236
|
+
google: {
|
|
237
|
+
provider: mockGoogleProvider, // Override with mock
|
|
238
|
+
clientId: "test-client-id",
|
|
239
|
+
clientSecret: "test-client-secret",
|
|
240
|
+
redirectUri: "http://localhost:3000/callback",
|
|
241
|
+
scopes: ["openid", "email"]
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
```
|
|
246
|
+
|
|
124
247
|
## Advanced Usage
|
|
125
248
|
|
|
126
|
-
### Custom
|
|
249
|
+
### Custom State Storage (StateDao)
|
|
250
|
+
|
|
251
|
+
By default, Lixa uses an in-memory cache for state management. For production applications or serverless environments, you should implement a custom state storage:
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
import { Lixa } from "@vunexa/lixa";
|
|
255
|
+
|
|
256
|
+
// Example: DynamoDB state storage
|
|
257
|
+
const dynamoDbStateDao = {
|
|
258
|
+
saveState: async (state: string, data: any, expiresInSeconds: number) => {
|
|
259
|
+
await dynamoDB.put({
|
|
260
|
+
TableName: "oauth-states",
|
|
261
|
+
Item: {
|
|
262
|
+
state,
|
|
263
|
+
data: JSON.stringify(data),
|
|
264
|
+
expiresAt: Math.floor(Date.now() / 1000) + expiresInSeconds,
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
getState: async (state: string) => {
|
|
270
|
+
const result = await dynamoDB.get({
|
|
271
|
+
TableName: "oauth-states",
|
|
272
|
+
Key: { state },
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
if (!result.Item) return null;
|
|
276
|
+
|
|
277
|
+
if (result.Item.expiresAt < Math.floor(Date.now() / 1000)) {
|
|
278
|
+
return null; // Expired
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return JSON.parse(result.Item.data);
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
deleteState: async (state: string) => {
|
|
285
|
+
await dynamoDB.delete({
|
|
286
|
+
TableName: "oauth-states",
|
|
287
|
+
Key: { state },
|
|
288
|
+
});
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const lixa = new Lixa({
|
|
293
|
+
providers: { /* ... */ },
|
|
294
|
+
stateDao: dynamoDbStateDao,
|
|
295
|
+
});
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**Important**: The state data includes a `codeVerifier` field required for PKCE. Make sure your storage preserves all fields in the data object.
|
|
299
|
+
|
|
300
|
+
### Custom Session Storage (SessionDao)
|
|
301
|
+
|
|
302
|
+
Similar to state storage, you can implement custom session storage:
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
const customSessionDao = {
|
|
306
|
+
saveSession: async (sessionId: string, session: any, expiresInSeconds: number) => {
|
|
307
|
+
// Store session in your database
|
|
308
|
+
await db.sessions.create({
|
|
309
|
+
id: sessionId,
|
|
310
|
+
data: session,
|
|
311
|
+
expiresAt: Date.now() + expiresInSeconds * 1000,
|
|
312
|
+
});
|
|
313
|
+
},
|
|
314
|
+
|
|
315
|
+
getSession: async (sessionId: string) => {
|
|
316
|
+
const session = await db.sessions.findById(sessionId);
|
|
317
|
+
if (!session || session.expiresAt < Date.now()) {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
return session.data;
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
deleteSession: async (sessionId: string) => {
|
|
324
|
+
await db.sessions.delete(sessionId);
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const lixa = new Lixa({
|
|
329
|
+
providers: { /* ... */ },
|
|
330
|
+
sessionDao: customSessionDao,
|
|
331
|
+
});
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Custom Session Strategy
|
|
127
335
|
|
|
128
|
-
|
|
336
|
+
The session strategy controls how user sessions are created from OAuth tokens:
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
const customSessionStrategy = {
|
|
340
|
+
createSession: async (tokenData) => {
|
|
341
|
+
// tokenData contains: access_token, refresh_token, id_token, etc.
|
|
342
|
+
|
|
343
|
+
// Decode ID token to get user info (for OIDC providers like Google)
|
|
344
|
+
let userInfo;
|
|
345
|
+
if (tokenData.id_token) {
|
|
346
|
+
const base64Payload = tokenData.id_token.split('.')[1];
|
|
347
|
+
const payload = Buffer.from(base64Payload, 'base64').toString();
|
|
348
|
+
userInfo = JSON.parse(payload);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Create or update user in your database
|
|
352
|
+
const user = await db.users.upsert({
|
|
353
|
+
email: userInfo.email,
|
|
354
|
+
name: userInfo.name,
|
|
355
|
+
// ... other fields
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// Create session in your database
|
|
359
|
+
const sessionId = generateUniqueId();
|
|
360
|
+
await db.sessions.create({
|
|
361
|
+
id: sessionId,
|
|
362
|
+
userId: user.id,
|
|
363
|
+
accessToken: tokenData.access_token,
|
|
364
|
+
refreshToken: tokenData.refresh_token,
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
token: sessionId,
|
|
369
|
+
raw: {
|
|
370
|
+
...tokenData,
|
|
371
|
+
userId: user.id,
|
|
372
|
+
userInfo,
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
},
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
const lixa = new Lixa({
|
|
379
|
+
providers: { /* ... */ },
|
|
380
|
+
sessionStrategy: customSessionStrategy,
|
|
381
|
+
});
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### Debug Mode
|
|
385
|
+
|
|
386
|
+
Enable debug logging to troubleshoot OAuth flows:
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
const lixa = new Lixa({
|
|
390
|
+
providers: { /* ... */ },
|
|
391
|
+
debug: true, // Enables detailed logging
|
|
392
|
+
});
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
Debug mode logs:
|
|
396
|
+
- Token exchange requests and responses
|
|
397
|
+
- State validation
|
|
398
|
+
- Session creation
|
|
399
|
+
- Error details from OAuth providers
|
|
400
|
+
|
|
401
|
+
### Legacy Provider Registration (Deprecated)
|
|
402
|
+
|
|
403
|
+
> **Note**: This pattern is deprecated. Use inline providers instead (see examples above).
|
|
404
|
+
|
|
405
|
+
For backward compatibility, you can still register providers globally:
|
|
129
406
|
|
|
130
407
|
```typescript
|
|
131
408
|
import { Lixa, IProvider } from "@vunexa/lixa";
|
|
@@ -136,12 +413,12 @@ class CustomProvider implements IProvider {
|
|
|
136
413
|
userInfoEndpoint = "https://custom-provider.com/api/user";
|
|
137
414
|
}
|
|
138
415
|
|
|
139
|
-
// Register the custom provider
|
|
416
|
+
// Register the custom provider BEFORE creating Lixa instance
|
|
140
417
|
Lixa.registerProvider({
|
|
141
418
|
custom: new CustomProvider(),
|
|
142
419
|
});
|
|
143
420
|
|
|
144
|
-
// Use it in your configuration
|
|
421
|
+
// Use it in your configuration (without provider field)
|
|
145
422
|
const lixa = new Lixa({
|
|
146
423
|
providers: {
|
|
147
424
|
custom: {
|
|
@@ -154,12 +431,82 @@ const lixa = new Lixa({
|
|
|
154
431
|
});
|
|
155
432
|
```
|
|
156
433
|
|
|
157
|
-
### Check Provider
|
|
434
|
+
### Check Provider Configuration
|
|
435
|
+
|
|
436
|
+
```typescript
|
|
437
|
+
// Check if a provider is configured for this instance
|
|
438
|
+
if (lixa.isProviderConfigured("google")) {
|
|
439
|
+
console.log("Google provider is configured");
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Get list of all registered providers (legacy)
|
|
443
|
+
const providers = Lixa.getRegisteredProviders();
|
|
444
|
+
console.log("Registered providers:", providers);
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
## Common Pitfalls & Solutions
|
|
448
|
+
|
|
449
|
+
### 1. "Invalid or expired state" Error
|
|
450
|
+
|
|
451
|
+
**Cause**: The state parameter is not being stored or retrieved correctly.
|
|
452
|
+
|
|
453
|
+
**Solution**: Implement a custom `stateDao` that persists state across requests. The default in-memory cache doesn't work in serverless or multi-instance environments.
|
|
454
|
+
|
|
455
|
+
### 2. "Missing code verifier" Error
|
|
456
|
+
|
|
457
|
+
**Cause**: The `codeVerifier` field is not being preserved in your state storage.
|
|
458
|
+
|
|
459
|
+
**Solution**: Ensure your `stateDao.saveState()` stores ALL fields from the data object, including `codeVerifier`:
|
|
158
460
|
|
|
159
461
|
```typescript
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
462
|
+
saveState: async (state: string, data: any, expiresInSeconds: number) => {
|
|
463
|
+
// ✅ Correct: Store the entire data object
|
|
464
|
+
await storage.save({ state, data, expiresAt: ... });
|
|
465
|
+
|
|
466
|
+
// ❌ Wrong: Only storing some fields
|
|
467
|
+
await storage.save({ state, provider: data.provider, expiresAt: ... });
|
|
468
|
+
}
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
### 3. Token Exchange Fails with 400 Bad Request
|
|
472
|
+
|
|
473
|
+
**Causes**:
|
|
474
|
+
- Redirect URI mismatch between your config and OAuth provider console
|
|
475
|
+
- Invalid client ID or secret
|
|
476
|
+
- Code has already been used or expired
|
|
477
|
+
|
|
478
|
+
**Solution**:
|
|
479
|
+
- Enable debug mode to see the exact error from the provider
|
|
480
|
+
- Verify redirect URI matches exactly (including protocol, port, path)
|
|
481
|
+
- Check that client credentials are correct
|
|
482
|
+
|
|
483
|
+
### 4. Session Not Found After Creation
|
|
484
|
+
|
|
485
|
+
**Cause**: Using default in-memory session storage in serverless/distributed environments.
|
|
486
|
+
|
|
487
|
+
**Solution**: Implement a custom `sessionDao` that uses persistent storage (database, Redis, etc.).
|
|
488
|
+
|
|
489
|
+
### 5. User Info is Undefined
|
|
490
|
+
|
|
491
|
+
**Cause**: The `sessionStrategy.createSession()` receives raw token data, not user info.
|
|
492
|
+
|
|
493
|
+
**Solution**: Decode the `id_token` (for OIDC providers) or fetch user info from the provider's API:
|
|
494
|
+
|
|
495
|
+
```typescript
|
|
496
|
+
createSession: async (tokenData) => {
|
|
497
|
+
// For OIDC providers (Google, etc.)
|
|
498
|
+
if (tokenData.id_token) {
|
|
499
|
+
const base64Payload = tokenData.id_token.split('.')[1];
|
|
500
|
+
const userInfo = JSON.parse(Buffer.from(base64Payload, 'base64').toString());
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// For OAuth-only providers (GitHub, etc.)
|
|
504
|
+
else if (tokenData.access_token) {
|
|
505
|
+
const response = await fetch(providerUserInfoEndpoint, {
|
|
506
|
+
headers: { Authorization: `Bearer ${tokenData.access_token}` }
|
|
507
|
+
});
|
|
508
|
+
const userInfo = await response.json();
|
|
509
|
+
}
|
|
163
510
|
}
|
|
164
511
|
```
|
|
165
512
|
|
|
@@ -173,33 +520,42 @@ if (Lixa.isProviderRegistered("google")) {
|
|
|
173
520
|
|
|
174
521
|
#### Static Methods
|
|
175
522
|
|
|
176
|
-
- `Lixa.
|
|
177
|
-
- `Lixa.
|
|
523
|
+
- `Lixa.generateRandomState(): string` - Generate a cryptographically secure random state string for CSRF protection
|
|
524
|
+
- `Lixa.registerProvider(providerMap: { [key: string]: IProvider })` - **[Deprecated]** Register custom providers globally. Use inline providers instead.
|
|
525
|
+
- `Lixa.getRegisteredProviders(): string[]` - **[Deprecated]** Get list of all registered provider names from legacy registry
|
|
178
526
|
|
|
179
527
|
#### Instance Methods
|
|
180
528
|
|
|
181
|
-
- `
|
|
182
|
-
- `
|
|
183
|
-
- `
|
|
529
|
+
- `getAuthUrl(provider: string, state: string): string` - Generate authorization URL for a provider. Automatically generates PKCE code verifier and challenge, stores state with code verifier for later validation.
|
|
530
|
+
- `handleCallback({ provider, code, state }): Promise<string>` - Handle OAuth callback, validate state parameter, retrieve PKCE code verifier, exchange authorization code for tokens, and create session using SessionStrategy. Returns session ID.
|
|
531
|
+
- `fetchSessionInfo(sessionId: string): Promise<Session | null>` - Retrieve session information by session ID from SessionDao.
|
|
532
|
+
- `isProviderConfigured(provider: string): boolean` - Check if a provider is configured for this Lixa instance. Useful for type guards and runtime validation.
|
|
184
533
|
|
|
185
534
|
### Types
|
|
186
535
|
|
|
187
536
|
```typescript
|
|
188
|
-
|
|
537
|
+
// Provider configuration with inline provider support
|
|
538
|
+
type ProviderConfig = {
|
|
189
539
|
clientId: string;
|
|
190
540
|
clientSecret: string;
|
|
191
541
|
redirectUri: string;
|
|
192
542
|
scopes: string[];
|
|
193
543
|
extraConfig?: Record<string, any>;
|
|
194
|
-
}
|
|
544
|
+
} & (
|
|
545
|
+
| { provider?: never } // Built-in provider (no provider field)
|
|
546
|
+
| { provider: IProvider } // Custom provider (provider field required)
|
|
547
|
+
);
|
|
195
548
|
|
|
196
|
-
interface LixaConfig {
|
|
197
|
-
providers:
|
|
549
|
+
interface LixaConfig<TProviders = Record<string, ProviderConfig>> {
|
|
550
|
+
providers: TProviders;
|
|
198
551
|
sessionStrategy?: SessionStrategy;
|
|
552
|
+
stateDao?: StateDao;
|
|
553
|
+
sessionDao?: SessionDao;
|
|
554
|
+
debug?: boolean;
|
|
199
555
|
}
|
|
200
556
|
|
|
201
557
|
interface SessionStrategy {
|
|
202
|
-
createSession(
|
|
558
|
+
createSession(tokenData: any): Promise<Session>;
|
|
203
559
|
}
|
|
204
560
|
|
|
205
561
|
interface Session {
|
|
@@ -207,6 +563,24 @@ interface Session {
|
|
|
207
563
|
raw: any;
|
|
208
564
|
}
|
|
209
565
|
|
|
566
|
+
interface StateData {
|
|
567
|
+
provider: string; // Provider name for routing
|
|
568
|
+
codeVerifier: string; // PKCE code verifier (64 hex chars)
|
|
569
|
+
createdAt: number; // Unix timestamp in milliseconds
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
interface StateDao {
|
|
573
|
+
saveState(state: string, data: StateData, expiresInSeconds: number): Promise<void>;
|
|
574
|
+
getState(state: string): Promise<StateData | null>;
|
|
575
|
+
deleteState(state: string): Promise<void>;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
interface SessionDao {
|
|
579
|
+
saveSession(sessionId: string, session: any, expiresInSeconds: number): Promise<void>;
|
|
580
|
+
getSession(sessionId: string): Promise<any | null>;
|
|
581
|
+
deleteSession(sessionId: string): Promise<void>;
|
|
582
|
+
}
|
|
583
|
+
|
|
210
584
|
interface IProvider {
|
|
211
585
|
authorizationEndpoint: string;
|
|
212
586
|
tokenEndpoint: string;
|
|
@@ -216,8 +590,322 @@ interface IProvider {
|
|
|
216
590
|
|
|
217
591
|
## Built-in Providers
|
|
218
592
|
|
|
219
|
-
-
|
|
220
|
-
|
|
593
|
+
Built-in providers are available in the `@vunexa/lixa-providers` package:
|
|
594
|
+
|
|
595
|
+
```bash
|
|
596
|
+
npm install @vunexa/lixa-providers
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
### Google (`GoogleProvider`)
|
|
600
|
+
|
|
601
|
+
```typescript
|
|
602
|
+
import { GoogleProvider } from "@vunexa/lixa-providers";
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
- **Type**: OAuth 2.0 + OpenID Connect (OIDC)
|
|
606
|
+
- **Authorization Endpoint**: `https://accounts.google.com/o/oauth2/v2/auth`
|
|
607
|
+
- **Token Endpoint**: `https://oauth2.googleapis.com/token`
|
|
608
|
+
- **User Info Endpoint**: `https://www.googleapis.com/oauth2/v2/userinfo`
|
|
609
|
+
- **Supports**: PKCE, ID tokens, refresh tokens
|
|
610
|
+
- **Common Scopes**:
|
|
611
|
+
- `openid` - Required for OpenID Connect
|
|
612
|
+
- `email` - Access to user's email address
|
|
613
|
+
- `profile` - Access to user's basic profile information
|
|
614
|
+
- `https://www.googleapis.com/auth/drive.readonly` - Read-only access to Google Drive
|
|
615
|
+
- `https://www.googleapis.com/auth/calendar.readonly` - Read-only access to Google Calendar
|
|
616
|
+
|
|
617
|
+
[Full list of Google OAuth scopes](https://developers.google.com/identity/protocols/oauth2/scopes)
|
|
618
|
+
|
|
619
|
+
### GitHub (`GithubProvider`)
|
|
620
|
+
|
|
621
|
+
```typescript
|
|
622
|
+
import { GithubProvider } from "@vunexa/lixa-providers";
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
- **Type**: OAuth 2.0
|
|
626
|
+
- **Authorization Endpoint**: `https://github.com/login/oauth/authorize`
|
|
627
|
+
- **Token Endpoint**: `https://github.com/login/oauth/access_token`
|
|
628
|
+
- **User Info Endpoint**: `https://api.github.com/user`
|
|
629
|
+
- **Supports**: PKCE
|
|
630
|
+
- **Common Scopes**:
|
|
631
|
+
- `user` - Read/write access to profile info
|
|
632
|
+
- `user:email` - Read access to user's email addresses
|
|
633
|
+
- `read:user` - Read-only access to profile info
|
|
634
|
+
- `repo` - Full control of private repositories
|
|
635
|
+
- `public_repo` - Access to public repositories
|
|
636
|
+
- `gist` - Create gists
|
|
637
|
+
- `read:org` - Read-only access to organization membership
|
|
638
|
+
|
|
639
|
+
[Full list of GitHub OAuth scopes](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps)
|
|
640
|
+
|
|
641
|
+
## Extensibility Interfaces
|
|
642
|
+
|
|
643
|
+
### SessionStrategy Interface
|
|
644
|
+
|
|
645
|
+
The `SessionStrategy` interface allows you to customize how OAuth tokens are converted into application sessions:
|
|
646
|
+
|
|
647
|
+
```typescript
|
|
648
|
+
interface SessionStrategy {
|
|
649
|
+
createSession(tokenData: any): Promise<Session>;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
interface Session {
|
|
653
|
+
token: string; // Your session identifier
|
|
654
|
+
raw: any; // Additional session data
|
|
655
|
+
}
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
**Use cases:**
|
|
659
|
+
- Decode ID tokens for user information
|
|
660
|
+
- Create or update users in your database
|
|
661
|
+
- Generate custom session identifiers
|
|
662
|
+
- Store tokens securely
|
|
663
|
+
- Add custom claims or metadata
|
|
664
|
+
|
|
665
|
+
**Example with database integration:**
|
|
666
|
+
```typescript
|
|
667
|
+
const dbSessionStrategy: SessionStrategy = {
|
|
668
|
+
createSession: async (tokenData) => {
|
|
669
|
+
// Decode ID token (for OIDC providers)
|
|
670
|
+
const payload = decodeJwt(tokenData.id_token);
|
|
671
|
+
|
|
672
|
+
// Create/update user
|
|
673
|
+
const user = await db.users.upsert({
|
|
674
|
+
email: payload.email,
|
|
675
|
+
name: payload.name
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
// Generate session
|
|
679
|
+
const sessionId = generateId();
|
|
680
|
+
await db.sessions.create({
|
|
681
|
+
id: sessionId,
|
|
682
|
+
userId: user.id,
|
|
683
|
+
accessToken: tokenData.access_token,
|
|
684
|
+
refreshToken: tokenData.refresh_token
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
return {
|
|
688
|
+
token: sessionId,
|
|
689
|
+
raw: { userId: user.id, ...tokenData }
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
### StateDao Interface
|
|
696
|
+
|
|
697
|
+
The `StateDao` interface manages OAuth state storage for CSRF protection and PKCE:
|
|
698
|
+
|
|
699
|
+
```typescript
|
|
700
|
+
interface StateDao {
|
|
701
|
+
saveState(state: string, data: StateData, expiresInSeconds: number): Promise<void>;
|
|
702
|
+
getState(state: string): Promise<StateData | null>;
|
|
703
|
+
deleteState(state: string): Promise<void>;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
interface StateData {
|
|
707
|
+
provider: string; // Provider name for routing
|
|
708
|
+
codeVerifier: string; // PKCE code verifier (64 hex chars)
|
|
709
|
+
createdAt: number; // Unix timestamp in milliseconds
|
|
710
|
+
}
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
**Required fields:**
|
|
714
|
+
- `provider`: Used to route callbacks to the correct provider configuration
|
|
715
|
+
- `codeVerifier`: Required for PKCE token exchange (RFC 7636)
|
|
716
|
+
- `createdAt`: Timestamp for debugging and validation
|
|
717
|
+
|
|
718
|
+
**Example with Redis:**
|
|
719
|
+
```typescript
|
|
720
|
+
const redisStateDao: StateDao = {
|
|
721
|
+
saveState: async (state, data, expiresInSeconds) => {
|
|
722
|
+
await redis.setex(
|
|
723
|
+
`oauth:state:${state}`,
|
|
724
|
+
expiresInSeconds,
|
|
725
|
+
JSON.stringify(data)
|
|
726
|
+
);
|
|
727
|
+
},
|
|
728
|
+
|
|
729
|
+
getState: async (state) => {
|
|
730
|
+
const data = await redis.get(`oauth:state:${state}`);
|
|
731
|
+
return data ? JSON.parse(data) : null;
|
|
732
|
+
},
|
|
733
|
+
|
|
734
|
+
deleteState: async (state) => {
|
|
735
|
+
await redis.del(`oauth:state:${state}`);
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
### SessionDao Interface
|
|
741
|
+
|
|
742
|
+
The `SessionDao` interface manages persistent user session storage:
|
|
743
|
+
|
|
744
|
+
```typescript
|
|
745
|
+
interface SessionDao {
|
|
746
|
+
saveSession(sessionId: string, data: any, expiresInSeconds: number): Promise<void>;
|
|
747
|
+
getSession(sessionId: string): Promise<any | null>;
|
|
748
|
+
deleteSession(sessionId: string): Promise<void>;
|
|
749
|
+
}
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
**Example with database:**
|
|
753
|
+
```typescript
|
|
754
|
+
const dbSessionDao: SessionDao = {
|
|
755
|
+
saveSession: async (sessionId, data, expiresInSeconds) => {
|
|
756
|
+
const expiresAt = new Date(Date.now() + expiresInSeconds * 1000);
|
|
757
|
+
await db.sessions.create({
|
|
758
|
+
id: sessionId,
|
|
759
|
+
data: JSON.stringify(data),
|
|
760
|
+
expiresAt
|
|
761
|
+
});
|
|
762
|
+
},
|
|
763
|
+
|
|
764
|
+
getSession: async (sessionId) => {
|
|
765
|
+
const session = await db.sessions.findOne({
|
|
766
|
+
id: sessionId,
|
|
767
|
+
expiresAt: { $gt: new Date() }
|
|
768
|
+
});
|
|
769
|
+
return session ? JSON.parse(session.data) : null;
|
|
770
|
+
},
|
|
771
|
+
|
|
772
|
+
deleteSession: async (sessionId) => {
|
|
773
|
+
await db.sessions.delete({ id: sessionId });
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
## PKCE Implementation
|
|
779
|
+
|
|
780
|
+
Lixa automatically implements **PKCE (Proof Key for Code Exchange)** according to [RFC 7636](https://tools.ietf.org/html/rfc7636) for all OAuth flows.
|
|
781
|
+
|
|
782
|
+
### How it Works
|
|
783
|
+
|
|
784
|
+
1. **Code Verifier Generation**
|
|
785
|
+
- Generates 32 cryptographically random bytes
|
|
786
|
+
- Encodes as 64-character hexadecimal string
|
|
787
|
+
- Example: `a1b2c3d4e5f6...` (64 chars)
|
|
788
|
+
|
|
789
|
+
2. **Code Challenge Generation**
|
|
790
|
+
- Creates SHA-256 hash of the code verifier
|
|
791
|
+
- Encodes as base64url (RFC 4648)
|
|
792
|
+
- Sends with authorization request
|
|
793
|
+
|
|
794
|
+
3. **Token Exchange**
|
|
795
|
+
- Retrieves code verifier from state storage
|
|
796
|
+
- Sends original code verifier with token request
|
|
797
|
+
- Provider validates: `SHA256(code_verifier) === code_challenge`
|
|
798
|
+
|
|
799
|
+
### Security Benefits
|
|
800
|
+
|
|
801
|
+
- **Prevents authorization code interception**: Even if an attacker intercepts the authorization code, they cannot exchange it for tokens without the code verifier
|
|
802
|
+
- **No client secret required**: PKCE works without client secrets, making it suitable for public clients
|
|
803
|
+
- **Standards compliant**: Follows RFC 7636 specification
|
|
804
|
+
|
|
805
|
+
### Debug Logging
|
|
806
|
+
|
|
807
|
+
Enable debug mode to see PKCE flow details:
|
|
808
|
+
|
|
809
|
+
```typescript
|
|
810
|
+
const lixa = new Lixa({
|
|
811
|
+
providers: { /* ... */ },
|
|
812
|
+
debug: true
|
|
813
|
+
});
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
Debug output includes:
|
|
817
|
+
- `[Lixa] [timestamp] [INFO] [Auth]` - Code verifier and challenge generation
|
|
818
|
+
- `[Lixa] [timestamp] [INFO] [State]` - State storage with code verifier
|
|
819
|
+
- `[Lixa] [timestamp] [INFO] [Token]` - Token exchange with code verifier
|
|
820
|
+
|
|
821
|
+
## Migration Guide
|
|
822
|
+
|
|
823
|
+
### Migrating from Legacy Registration Pattern
|
|
824
|
+
|
|
825
|
+
**Old Pattern (Deprecated):**
|
|
826
|
+
```typescript
|
|
827
|
+
import { Lixa, IProvider } from "@vunexa/lixa";
|
|
828
|
+
import { GoogleProvider } from "@vunexa/lixa";
|
|
829
|
+
|
|
830
|
+
// Step 1: Register providers globally
|
|
831
|
+
Lixa.registerProvider({
|
|
832
|
+
google: new GoogleProvider()
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
// Step 2: Configure with credentials only
|
|
836
|
+
const lixa = new Lixa({
|
|
837
|
+
providers: {
|
|
838
|
+
google: {
|
|
839
|
+
clientId: "...",
|
|
840
|
+
clientSecret: "...",
|
|
841
|
+
redirectUri: "...",
|
|
842
|
+
scopes: ["openid", "email"]
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
**New Pattern (Recommended):**
|
|
849
|
+
```typescript
|
|
850
|
+
import { Lixa } from "@vunexa/lixa";
|
|
851
|
+
import { GoogleProvider } from "@vunexa/lixa-providers";
|
|
852
|
+
|
|
853
|
+
// Single step: Pass provider inline
|
|
854
|
+
const lixa = new Lixa({
|
|
855
|
+
providers: {
|
|
856
|
+
google: {
|
|
857
|
+
provider: new GoogleProvider(), // ← Add this line
|
|
858
|
+
clientId: "...",
|
|
859
|
+
clientSecret: "...",
|
|
860
|
+
redirectUri: "...",
|
|
861
|
+
scopes: ["openid", "email"]
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
**Migration Steps:**
|
|
868
|
+
1. Install `@vunexa/lixa-providers`: `npm install @vunexa/lixa-providers`
|
|
869
|
+
2. Update imports: `import { GoogleProvider } from "@vunexa/lixa-providers"`
|
|
870
|
+
3. Remove `Lixa.registerProvider()` calls
|
|
871
|
+
4. Add `provider` field to each provider configuration
|
|
872
|
+
5. Test your OAuth flows
|
|
873
|
+
|
|
874
|
+
**Benefits:**
|
|
875
|
+
- ✅ No global state - each Lixa instance is independent
|
|
876
|
+
- ✅ Better testability - easy to mock providers per instance
|
|
877
|
+
- ✅ Clearer configuration - everything in one place
|
|
878
|
+
- ✅ Type safety - TypeScript knows which providers are configured
|
|
879
|
+
|
|
880
|
+
## Security Considerations
|
|
881
|
+
|
|
882
|
+
### PKCE (Proof Key for Code Exchange)
|
|
883
|
+
|
|
884
|
+
Lixa automatically implements PKCE for all OAuth flows:
|
|
885
|
+
- Generates a cryptographically secure code verifier
|
|
886
|
+
- Creates SHA-256 code challenge
|
|
887
|
+
- Stores code verifier with state
|
|
888
|
+
- Sends code verifier during token exchange
|
|
889
|
+
|
|
890
|
+
This protects against authorization code interception attacks.
|
|
891
|
+
|
|
892
|
+
### State Parameter
|
|
893
|
+
|
|
894
|
+
The state parameter prevents CSRF attacks:
|
|
895
|
+
- Always generated using cryptographically secure random bytes
|
|
896
|
+
- Must be validated on callback
|
|
897
|
+
- Automatically stored and validated when using custom `stateDao`
|
|
898
|
+
- Single-use (deleted after validation)
|
|
899
|
+
|
|
900
|
+
### Best Practices
|
|
901
|
+
|
|
902
|
+
1. **Always use HTTPS** in production for redirect URIs
|
|
903
|
+
2. **Implement custom storage** (StateDao/SessionDao) for production
|
|
904
|
+
3. **Set short TTLs** for state (5 minutes) and sessions (24 hours recommended)
|
|
905
|
+
4. **Validate state parameter** on every callback
|
|
906
|
+
5. **Store tokens securely** - never expose access/refresh tokens to client-side code
|
|
907
|
+
6. **Use HttpOnly cookies** for session tokens
|
|
908
|
+
7. **Enable debug mode** only in development
|
|
221
909
|
|
|
222
910
|
## Development
|
|
223
911
|
|