@vunexa/lixa 0.0.1-alpha.29 → 0.0.1-alpha.33
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 +297 -249
- package/dist/dao/session-cache.d.ts +6 -7
- package/dist/dao/session-cache.d.ts.map +1 -1
- package/dist/dao/state-cache.d.ts +8 -6
- package/dist/dao/state-cache.d.ts.map +1 -1
- package/dist/dao/types.d.ts +262 -145
- package/dist/dao/types.d.ts.map +1 -1
- package/dist/export-types/index.d.ts +305 -345
- package/dist/index.cjs +98 -70
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +303 -343
- package/dist/index.d.ts +2 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +96 -67
- package/dist/index.js.map +1 -1
- package/dist/lixa.d.ts +5 -7
- package/dist/lixa.d.ts.map +1 -1
- package/dist/types.d.ts +41 -24
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,14 +13,16 @@ A flexible, provider-agnostic OAuth 2.0 and OpenID Connect (OIDC) client library
|
|
|
13
13
|
|
|
14
14
|
## Features
|
|
15
15
|
|
|
16
|
-
- Multi-provider OAuth/OIDC support with unified API
|
|
17
|
-
- Built-in
|
|
18
|
-
- Custom provider
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
16
|
+
- **Multi-provider OAuth/OIDC support** with unified API
|
|
17
|
+
- **Built-in providers** for Google, GitHub, and more via `@vunexa/lixa-providers`
|
|
18
|
+
- **Custom provider support** with extensible provider interface
|
|
19
|
+
- **Unified session management** via `SessionHandler` (generation + storage in one place)
|
|
20
|
+
- **Unified state management** via `StateHandler` (generation + storage in one place)
|
|
21
|
+
- **Automatic PKCE** (Proof Key for Code Exchange) for all OAuth flows
|
|
22
|
+
- **User info utilities** for extracting user data from OAuth tokens
|
|
23
|
+
- **TypeScript-first** with full type safety (zero `any` types)
|
|
24
|
+
- **OAuth 2.0 and OpenID Connect** spec-compliant types
|
|
25
|
+
- **100% test coverage** with comprehensive error handling
|
|
24
26
|
|
|
25
27
|
---
|
|
26
28
|
|
|
@@ -68,9 +70,10 @@ const lixa = new Lixa({
|
|
|
68
70
|
},
|
|
69
71
|
},
|
|
70
72
|
|
|
71
|
-
// Optional: custom session
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
// Optional: custom session handler
|
|
74
|
+
sessionHandler: {
|
|
75
|
+
// Optional: customize session generation
|
|
76
|
+
generateSession: async (tokenData, providerMetadata) => {
|
|
74
77
|
// tokenData is typed as OAuthTokenResponse with proper OAuth 2.0 fields
|
|
75
78
|
// { access_token, token_type, expires_in?, refresh_token?, scope?, id_token? }
|
|
76
79
|
return {
|
|
@@ -78,6 +81,19 @@ const lixa = new Lixa({
|
|
|
78
81
|
raw: tokenData,
|
|
79
82
|
};
|
|
80
83
|
},
|
|
84
|
+
// Optional: custom session storage
|
|
85
|
+
sessionStorage: {
|
|
86
|
+
saveSession: async (sessionId, session, ttl) => {
|
|
87
|
+
await db.sessions.create({ id: sessionId, data: session, expiresAt: Date.now() + ttl * 1000 });
|
|
88
|
+
},
|
|
89
|
+
getSession: async (sessionId) => {
|
|
90
|
+
const session = await db.sessions.findOne({ id: sessionId });
|
|
91
|
+
return session?.data || null;
|
|
92
|
+
},
|
|
93
|
+
deleteSession: async (sessionId) => {
|
|
94
|
+
await db.sessions.delete({ id: sessionId });
|
|
95
|
+
},
|
|
96
|
+
},
|
|
81
97
|
},
|
|
82
98
|
});
|
|
83
99
|
```
|
|
@@ -85,14 +101,11 @@ const lixa = new Lixa({
|
|
|
85
101
|
### 3. Redirect users to the provider's authorization URL
|
|
86
102
|
|
|
87
103
|
```typescript
|
|
88
|
-
app.get("/login", (req, res) => {
|
|
104
|
+
app.get("/login", async (req, res) => {
|
|
89
105
|
const provider = req.query.provider as string; // 'google' or 'github'
|
|
90
|
-
const state = Lixa.generateRandomState();
|
|
91
|
-
|
|
92
|
-
// Store state in session for validation
|
|
93
|
-
req.session.oauthState = state;
|
|
94
106
|
|
|
95
|
-
|
|
107
|
+
// getAuthUrl automatically generates state if not provided
|
|
108
|
+
const authUrl = await lixa.getAuthUrl(provider);
|
|
96
109
|
res.redirect(authUrl);
|
|
97
110
|
});
|
|
98
111
|
```
|
|
@@ -105,11 +118,7 @@ app.get("/auth/:provider/callback", async (req, res) => {
|
|
|
105
118
|
const provider = req.params.provider;
|
|
106
119
|
|
|
107
120
|
try {
|
|
108
|
-
//
|
|
109
|
-
if (state !== req.session.oauthState) {
|
|
110
|
-
throw new Error("Invalid state parameter");
|
|
111
|
-
}
|
|
112
|
-
|
|
121
|
+
// State validation is handled automatically by lixa
|
|
113
122
|
const sessionId = await lixa.handleCallback({
|
|
114
123
|
provider,
|
|
115
124
|
code: code as string,
|
|
@@ -248,15 +257,15 @@ const lixa = new Lixa({
|
|
|
248
257
|
|
|
249
258
|
## Advanced Usage
|
|
250
259
|
|
|
251
|
-
### Custom State
|
|
260
|
+
### Custom State Handler
|
|
252
261
|
|
|
253
|
-
By default, Lixa uses an in-memory cache for state management. For production applications or serverless environments, you should implement a custom state
|
|
262
|
+
By default, Lixa uses an in-memory cache for state management. For production applications or serverless environments, you should implement a custom state handler:
|
|
254
263
|
|
|
255
264
|
```typescript
|
|
256
|
-
import { Lixa,
|
|
265
|
+
import { Lixa, StateHandler, StateStorage, StateData } from "@vunexa/lixa";
|
|
257
266
|
|
|
258
|
-
// Example: DynamoDB state storage
|
|
259
|
-
const
|
|
267
|
+
// Example: DynamoDB state storage
|
|
268
|
+
const dynamoDbStateStorage: StateStorage = {
|
|
260
269
|
saveState: async (state: string, data: StateData, expiresInSeconds: number) => {
|
|
261
270
|
await dynamoDB.put({
|
|
262
271
|
TableName: "oauth-states",
|
|
@@ -293,27 +302,32 @@ const dynamoDbStateDao: StateDao = {
|
|
|
293
302
|
|
|
294
303
|
const lixa = new Lixa({
|
|
295
304
|
providers: { /* ... */ },
|
|
296
|
-
|
|
305
|
+
stateHandler: {
|
|
306
|
+
stateStorage: dynamoDbStateStorage,
|
|
307
|
+
// Optional: customize state generation
|
|
308
|
+
// generateState: async (provider) => { ... }
|
|
309
|
+
},
|
|
297
310
|
});
|
|
298
311
|
```
|
|
299
312
|
|
|
300
|
-
**
|
|
313
|
+
**StateHandler Properties:**
|
|
314
|
+
- `stateStorage` (optional): Custom storage for OAuth state (save/get/delete operations)
|
|
315
|
+
- `generateState` (optional): Custom state and PKCE verifier generation
|
|
316
|
+
|
|
317
|
+
**StateData Fields:**
|
|
301
318
|
- `provider` (string): Provider name for callback routing
|
|
302
319
|
- `codeVerifier` (string): PKCE code verifier (64 hex characters)
|
|
303
320
|
- `createdAt` (number): Unix timestamp in milliseconds
|
|
304
321
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
### Custom Session Storage (SessionDao)
|
|
322
|
+
### Custom Session Handler
|
|
308
323
|
|
|
309
|
-
|
|
324
|
+
The session handler manages both session generation and storage. You can customize either or both:
|
|
310
325
|
|
|
311
326
|
```typescript
|
|
312
|
-
import { Lixa,
|
|
327
|
+
import { Lixa, SessionHandler, SessionStorage, extractUserInfo } from "@vunexa/lixa";
|
|
313
328
|
|
|
314
|
-
const
|
|
329
|
+
const customSessionStorage: SessionStorage = {
|
|
315
330
|
saveSession: async <T = unknown>(sessionId: string, session: T, expiresInSeconds: number) => {
|
|
316
|
-
// Store session in your database
|
|
317
331
|
await db.sessions.create({
|
|
318
332
|
id: sessionId,
|
|
319
333
|
data: session,
|
|
@@ -336,16 +350,39 @@ const customSessionDao: SessionDao = {
|
|
|
336
350
|
|
|
337
351
|
const lixa = new Lixa({
|
|
338
352
|
providers: { /* ... */ },
|
|
339
|
-
|
|
353
|
+
sessionHandler: {
|
|
354
|
+
// Optional: customize session generation
|
|
355
|
+
generateSession: async (tokenData, providerMetadata) => {
|
|
356
|
+
// Extract user info from OAuth tokens
|
|
357
|
+
const { userInfo } = await extractUserInfo(tokenData, providerMetadata);
|
|
358
|
+
|
|
359
|
+
// Create or update user in database
|
|
360
|
+
const user = await db.users.upsert({
|
|
361
|
+
email: userInfo.email,
|
|
362
|
+
name: userInfo.name,
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
token: tokenData.access_token,
|
|
367
|
+
raw: { ...tokenData, userId: user.id },
|
|
368
|
+
};
|
|
369
|
+
},
|
|
370
|
+
// Optional: custom session storage
|
|
371
|
+
sessionStorage: customSessionStorage,
|
|
372
|
+
},
|
|
340
373
|
});
|
|
341
374
|
```
|
|
342
375
|
|
|
343
|
-
|
|
376
|
+
**SessionHandler Properties:**
|
|
377
|
+
- `generateSession` (optional): Customize how OAuth tokens are converted to session data
|
|
378
|
+
- `sessionStorage` (optional): Custom storage for sessions (save/get/delete operations)
|
|
379
|
+
|
|
380
|
+
### Custom Session Generation
|
|
344
381
|
|
|
345
|
-
The
|
|
382
|
+
The `generateSession` method controls how user sessions are created from OAuth tokens:
|
|
346
383
|
|
|
347
384
|
```typescript
|
|
348
|
-
import { Lixa,
|
|
385
|
+
import { Lixa, SessionHandler, OAuthTokenResponse, Session, extractUserInfo } from "@vunexa/lixa";
|
|
349
386
|
|
|
350
387
|
// Define custom session data structure
|
|
351
388
|
interface CustomSessionData extends OAuthTokenResponse {
|
|
@@ -356,8 +393,8 @@ interface CustomSessionData extends OAuthTokenResponse {
|
|
|
356
393
|
};
|
|
357
394
|
}
|
|
358
395
|
|
|
359
|
-
const
|
|
360
|
-
|
|
396
|
+
const customSessionHandler: SessionHandler = {
|
|
397
|
+
generateSession: async (tokenData: OAuthTokenResponse, providerMetadata): Promise<Session<CustomSessionData>> => {
|
|
361
398
|
// tokenData is properly typed with OAuth 2.0 fields:
|
|
362
399
|
// - access_token: string (required)
|
|
363
400
|
// - token_type: string (required)
|
|
@@ -366,13 +403,8 @@ const customSessionStrategy: SessionStrategy = {
|
|
|
366
403
|
// - scope?: string
|
|
367
404
|
// - id_token?: string (for OIDC providers)
|
|
368
405
|
|
|
369
|
-
//
|
|
370
|
-
|
|
371
|
-
if (tokenData.id_token) {
|
|
372
|
-
const base64Payload = tokenData.id_token.split('.')[1];
|
|
373
|
-
const payload = Buffer.from(base64Payload, 'base64').toString();
|
|
374
|
-
userInfo = JSON.parse(payload);
|
|
375
|
-
}
|
|
406
|
+
// Extract user info using lixa's utility
|
|
407
|
+
const { userInfo } = await extractUserInfo(tokenData, providerMetadata);
|
|
376
408
|
|
|
377
409
|
// Create or update user in your database
|
|
378
410
|
const user = await db.users.upsert({
|
|
@@ -381,17 +413,8 @@ const customSessionStrategy: SessionStrategy = {
|
|
|
381
413
|
// ... other fields
|
|
382
414
|
});
|
|
383
415
|
|
|
384
|
-
// Create session in your database
|
|
385
|
-
const sessionId = generateUniqueId();
|
|
386
|
-
await db.sessions.create({
|
|
387
|
-
id: sessionId,
|
|
388
|
-
userId: user.id,
|
|
389
|
-
accessToken: tokenData.access_token,
|
|
390
|
-
refreshToken: tokenData.refresh_token,
|
|
391
|
-
});
|
|
392
|
-
|
|
393
416
|
return {
|
|
394
|
-
token:
|
|
417
|
+
token: tokenData.access_token,
|
|
395
418
|
raw: {
|
|
396
419
|
...tokenData,
|
|
397
420
|
userId: user.id,
|
|
@@ -399,11 +422,25 @@ const customSessionStrategy: SessionStrategy = {
|
|
|
399
422
|
},
|
|
400
423
|
};
|
|
401
424
|
},
|
|
425
|
+
|
|
426
|
+
// Optional: custom storage
|
|
427
|
+
sessionStorage: {
|
|
428
|
+
saveSession: async (sessionId, session, ttl) => {
|
|
429
|
+
await db.sessions.create({ id: sessionId, data: session, expiresAt: Date.now() + ttl * 1000 });
|
|
430
|
+
},
|
|
431
|
+
getSession: async (sessionId) => {
|
|
432
|
+
const session = await db.sessions.findOne({ id: sessionId });
|
|
433
|
+
return session?.data || null;
|
|
434
|
+
},
|
|
435
|
+
deleteSession: async (sessionId) => {
|
|
436
|
+
await db.sessions.delete({ id: sessionId });
|
|
437
|
+
},
|
|
438
|
+
},
|
|
402
439
|
};
|
|
403
440
|
|
|
404
441
|
const lixa = new Lixa({
|
|
405
442
|
providers: { /* ... */ },
|
|
406
|
-
|
|
443
|
+
sessionHandler: customSessionHandler,
|
|
407
444
|
});
|
|
408
445
|
```
|
|
409
446
|
|
|
@@ -476,21 +513,36 @@ console.log("Registered providers:", providers);
|
|
|
476
513
|
|
|
477
514
|
**Cause**: The state parameter is not being stored or retrieved correctly.
|
|
478
515
|
|
|
479
|
-
**Solution**: Implement a custom `
|
|
516
|
+
**Solution**: Implement a custom `stateHandler` with persistent storage. The default in-memory cache doesn't work in serverless or multi-instance environments.
|
|
517
|
+
|
|
518
|
+
```typescript
|
|
519
|
+
const lixa = new Lixa({
|
|
520
|
+
providers: { /* ... */ },
|
|
521
|
+
stateHandler: {
|
|
522
|
+
stateStorage: {
|
|
523
|
+
saveState: async (state, data, ttl) => await redis.setex(state, ttl, JSON.stringify(data)),
|
|
524
|
+
getState: async (state) => JSON.parse(await redis.get(state) || 'null'),
|
|
525
|
+
deleteState: async (state) => await redis.del(state),
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
```
|
|
480
530
|
|
|
481
531
|
### 2. "Missing code verifier" Error
|
|
482
532
|
|
|
483
533
|
**Cause**: The `codeVerifier` field is not being preserved in your state storage.
|
|
484
534
|
|
|
485
|
-
**Solution**: Ensure your `
|
|
535
|
+
**Solution**: Ensure your `stateStorage.saveState()` stores ALL fields from the data object, including `codeVerifier`:
|
|
486
536
|
|
|
487
537
|
```typescript
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
538
|
+
stateStorage: {
|
|
539
|
+
saveState: async (state: string, data: StateData, expiresInSeconds: number) => {
|
|
540
|
+
// ✅ Correct: Store the entire data object
|
|
541
|
+
await storage.save({ state, data, expiresAt: ... });
|
|
542
|
+
|
|
543
|
+
// ❌ Wrong: Only storing some fields
|
|
544
|
+
await storage.save({ state, provider: data.provider, expiresAt: ... });
|
|
545
|
+
}
|
|
494
546
|
}
|
|
495
547
|
```
|
|
496
548
|
|
|
@@ -510,32 +562,37 @@ saveState: async (state: string, data: any, expiresInSeconds: number) => {
|
|
|
510
562
|
|
|
511
563
|
**Cause**: Using default in-memory session storage in serverless/distributed environments.
|
|
512
564
|
|
|
513
|
-
**Solution**: Implement a custom `
|
|
565
|
+
**Solution**: Implement a custom `sessionHandler` with persistent storage (database, Redis, etc.):
|
|
566
|
+
|
|
567
|
+
```typescript
|
|
568
|
+
const lixa = new Lixa({
|
|
569
|
+
providers: { /* ... */ },
|
|
570
|
+
sessionHandler: {
|
|
571
|
+
sessionStorage: {
|
|
572
|
+
saveSession: async (id, session, ttl) => await db.sessions.create({ id, session, ttl }),
|
|
573
|
+
getSession: async (id) => await db.sessions.findOne({ id }),
|
|
574
|
+
deleteSession: async (id) => await db.sessions.delete({ id }),
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
});
|
|
578
|
+
```
|
|
514
579
|
|
|
515
580
|
### 5. User Info is Undefined
|
|
516
581
|
|
|
517
|
-
**Cause**: The `
|
|
582
|
+
**Cause**: The `generateSession()` receives raw token data, not user info.
|
|
518
583
|
|
|
519
|
-
**Solution**:
|
|
584
|
+
**Solution**: Use lixa's `extractUserInfo()` utility to automatically extract user info:
|
|
520
585
|
|
|
521
586
|
```typescript
|
|
522
|
-
import {
|
|
523
|
-
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
//
|
|
527
|
-
|
|
528
|
-
const base64Payload = tokenData.id_token.split('.')[1];
|
|
529
|
-
const userInfo = JSON.parse(Buffer.from(base64Payload, 'base64').toString());
|
|
530
|
-
}
|
|
587
|
+
import { SessionHandler, extractUserInfo } from "@vunexa/lixa";
|
|
588
|
+
|
|
589
|
+
const sessionHandler: SessionHandler = {
|
|
590
|
+
generateSession: async (tokenData, providerMetadata) => {
|
|
591
|
+
// Automatically extracts user info from ID token or fetches from API
|
|
592
|
+
const { userInfo } = await extractUserInfo(tokenData, providerMetadata);
|
|
531
593
|
|
|
532
|
-
//
|
|
533
|
-
|
|
534
|
-
const response = await fetch(providerUserInfoEndpoint, {
|
|
535
|
-
headers: { Authorization: `Bearer ${tokenData.access_token}` }
|
|
536
|
-
});
|
|
537
|
-
const userInfo = await response.json();
|
|
538
|
-
}
|
|
594
|
+
// Now you have user info
|
|
595
|
+
console.log(userInfo.email, userInfo.name);
|
|
539
596
|
|
|
540
597
|
return {
|
|
541
598
|
token: tokenData.access_token,
|
|
@@ -556,9 +613,9 @@ High-level function that automatically extracts user information from OAuth toke
|
|
|
556
613
|
```typescript
|
|
557
614
|
import { extractUserInfo, type UserInfo } from "@vunexa/lixa";
|
|
558
615
|
|
|
559
|
-
// In your session
|
|
560
|
-
const
|
|
561
|
-
|
|
616
|
+
// In your session handler
|
|
617
|
+
const sessionHandler = {
|
|
618
|
+
generateSession: async (tokenData, providerMetadata) => {
|
|
562
619
|
// Automatically extracts user info from ID token or fetches from API
|
|
563
620
|
const { userInfo } = await extractUserInfo(tokenData, providerMetadata);
|
|
564
621
|
|
|
@@ -684,8 +741,8 @@ const lixa = new Lixa({
|
|
|
684
741
|
}
|
|
685
742
|
},
|
|
686
743
|
|
|
687
|
-
|
|
688
|
-
|
|
744
|
+
sessionHandler: {
|
|
745
|
+
generateSession: async (tokenData, providerMetadata) => {
|
|
689
746
|
// Extract user info - works for both Google (ID token) and GitHub (API fetch)
|
|
690
747
|
const { userInfo } = await extractUserInfo(tokenData, providerMetadata);
|
|
691
748
|
|
|
@@ -701,26 +758,32 @@ const lixa = new Lixa({
|
|
|
701
758
|
emailVerified: userInfo.email_verified,
|
|
702
759
|
});
|
|
703
760
|
|
|
704
|
-
// Create session
|
|
705
|
-
const sessionId = generateUniqueId();
|
|
706
|
-
await db.sessions.create({
|
|
707
|
-
id: sessionId,
|
|
708
|
-
userId: user.id,
|
|
709
|
-
accessToken: tokenData.access_token,
|
|
710
|
-
refreshToken: tokenData.refresh_token,
|
|
711
|
-
expiresAt: tokenData.expires_in
|
|
712
|
-
? new Date(Date.now() + tokenData.expires_in * 1000)
|
|
713
|
-
: null,
|
|
714
|
-
});
|
|
715
|
-
|
|
716
761
|
return {
|
|
717
|
-
token:
|
|
762
|
+
token: tokenData.access_token,
|
|
718
763
|
raw: {
|
|
719
764
|
...tokenData,
|
|
720
765
|
userId: user.id,
|
|
721
766
|
}
|
|
722
767
|
};
|
|
723
|
-
}
|
|
768
|
+
},
|
|
769
|
+
sessionStorage: {
|
|
770
|
+
saveSession: async (sessionId, session, ttl) => {
|
|
771
|
+
await db.sessions.create({
|
|
772
|
+
id: sessionId,
|
|
773
|
+
userId: session.raw.userId,
|
|
774
|
+
accessToken: session.raw.access_token,
|
|
775
|
+
refreshToken: session.raw.refresh_token,
|
|
776
|
+
expiresAt: new Date(Date.now() + ttl * 1000),
|
|
777
|
+
});
|
|
778
|
+
},
|
|
779
|
+
getSession: async (sessionId) => {
|
|
780
|
+
const session = await db.sessions.findOne({ id: sessionId });
|
|
781
|
+
return session ? { token: session.accessToken, raw: session } : null;
|
|
782
|
+
},
|
|
783
|
+
deleteSession: async (sessionId) => {
|
|
784
|
+
await db.sessions.delete({ id: sessionId });
|
|
785
|
+
},
|
|
786
|
+
},
|
|
724
787
|
}
|
|
725
788
|
});
|
|
726
789
|
```
|
|
@@ -744,15 +807,14 @@ const lixa = new Lixa({
|
|
|
744
807
|
|
|
745
808
|
#### Static Methods
|
|
746
809
|
|
|
747
|
-
- `Lixa.generateRandomState(): string` - Generate a cryptographically secure random state string for CSRF protection
|
|
748
810
|
- `Lixa.registerProvider(providerMap: { [key: string]: IProvider })` - **[Deprecated]** Register custom providers globally. Use inline providers instead.
|
|
749
811
|
- `Lixa.getRegisteredProviders(): string[]` - **[Deprecated]** Get list of all registered provider names from legacy registry
|
|
750
812
|
|
|
751
813
|
#### Instance Methods
|
|
752
814
|
|
|
753
|
-
- `getAuthUrl(provider: string, state
|
|
754
|
-
- `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
|
|
755
|
-
- `fetchSessionInfo(sessionId: string): Promise<Session | null>` - Retrieve session information by session ID from
|
|
815
|
+
- `getAuthUrl(provider: string, state?: string): Promise<string>` - Generate authorization URL for a provider. Automatically generates state if not provided, creates PKCE code verifier and challenge, stores state with code verifier for later validation.
|
|
816
|
+
- `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 SessionHandler. Returns session ID.
|
|
817
|
+
- `fetchSessionInfo(sessionId: string): Promise<Session | null>` - Retrieve session information by session ID from SessionHandler storage.
|
|
756
818
|
- `isProviderConfigured(provider: string): boolean` - Check if a provider is configured for this Lixa instance. Useful for type guards and runtime validation.
|
|
757
819
|
|
|
758
820
|
### Types
|
|
@@ -785,14 +847,34 @@ type ProviderConfig = {
|
|
|
785
847
|
|
|
786
848
|
interface LixaConfig<TProviders = Record<string, ProviderConfig>> {
|
|
787
849
|
providers: TProviders;
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
sessionDao?: SessionDao;
|
|
850
|
+
sessionHandler?: SessionHandler;
|
|
851
|
+
stateHandler?: StateHandler;
|
|
791
852
|
debug?: boolean;
|
|
792
853
|
}
|
|
793
854
|
|
|
794
|
-
interface
|
|
795
|
-
|
|
855
|
+
interface SessionHandler {
|
|
856
|
+
generateSession?<T extends Session>(
|
|
857
|
+
tokenData: OAuthTokenResponse,
|
|
858
|
+
providerMetadata: ProviderMetadata
|
|
859
|
+
): Promise<T>;
|
|
860
|
+
sessionStorage?: SessionStorage;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
interface SessionStorage {
|
|
864
|
+
saveSession<T extends Session>(sessionId: string, session: T, expiresInSeconds: number): Promise<void>;
|
|
865
|
+
getSession<T extends Session>(sessionId: string): Promise<T | null>;
|
|
866
|
+
deleteSession(sessionId: string): Promise<void>;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
interface StateHandler {
|
|
870
|
+
generateState?(provider: string): Promise<{ state: string; data: StateData }>;
|
|
871
|
+
stateStorage?: StateStorage;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
interface StateStorage {
|
|
875
|
+
saveState(state: string, data: StateData, expiresInSeconds: number): Promise<void>;
|
|
876
|
+
getState(state: string): Promise<StateData | null>;
|
|
877
|
+
deleteState(state: string): Promise<void>;
|
|
796
878
|
}
|
|
797
879
|
|
|
798
880
|
interface Session<TRaw = OAuthTokenResponse> {
|
|
@@ -806,16 +888,13 @@ interface StateData {
|
|
|
806
888
|
createdAt: number; // Unix timestamp in milliseconds
|
|
807
889
|
}
|
|
808
890
|
|
|
809
|
-
interface
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
saveSession<T = unknown>(sessionId: string, session: T, expiresInSeconds: number): Promise<void>;
|
|
817
|
-
getSession<T = unknown>(sessionId: string): Promise<T | null>;
|
|
818
|
-
deleteSession(sessionId: string): Promise<void>;
|
|
891
|
+
interface ProviderMetadata {
|
|
892
|
+
name: string;
|
|
893
|
+
endpoints: {
|
|
894
|
+
authorization: string;
|
|
895
|
+
token: string;
|
|
896
|
+
userInfo: string;
|
|
897
|
+
};
|
|
819
898
|
}
|
|
820
899
|
|
|
821
900
|
interface IProvider {
|
|
@@ -877,13 +956,23 @@ import { GithubProvider } from "@vunexa/lixa-providers";
|
|
|
877
956
|
|
|
878
957
|
## Extensibility Interfaces
|
|
879
958
|
|
|
880
|
-
###
|
|
959
|
+
### SessionHandler Interface
|
|
881
960
|
|
|
882
|
-
The `
|
|
961
|
+
The `SessionHandler` interface allows you to customize session generation and storage:
|
|
883
962
|
|
|
884
963
|
```typescript
|
|
885
|
-
interface
|
|
886
|
-
|
|
964
|
+
interface SessionHandler {
|
|
965
|
+
generateSession?<T extends Session>(
|
|
966
|
+
tokenData: OAuthTokenResponse,
|
|
967
|
+
providerMetadata: ProviderMetadata
|
|
968
|
+
): Promise<T>;
|
|
969
|
+
sessionStorage?: SessionStorage;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
interface SessionStorage {
|
|
973
|
+
saveSession<T extends Session>(sessionId: string, session: T, expiresInSeconds: number): Promise<void>;
|
|
974
|
+
getSession<T extends Session>(sessionId: string): Promise<T | null>;
|
|
975
|
+
deleteSession(sessionId: string): Promise<void>;
|
|
887
976
|
}
|
|
888
977
|
|
|
889
978
|
interface Session<TRaw = OAuthTokenResponse> {
|
|
@@ -900,68 +989,84 @@ interface OAuthTokenResponse {
|
|
|
900
989
|
id_token?: string; // Optional (OIDC)
|
|
901
990
|
[key: string]: unknown; // Provider-specific fields
|
|
902
991
|
}
|
|
992
|
+
|
|
993
|
+
interface ProviderMetadata {
|
|
994
|
+
name: string;
|
|
995
|
+
endpoints: {
|
|
996
|
+
authorization: string;
|
|
997
|
+
token: string;
|
|
998
|
+
userInfo: string;
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
903
1001
|
```
|
|
904
1002
|
|
|
905
1003
|
**Use cases:**
|
|
906
|
-
-
|
|
1004
|
+
- Extract user info from OAuth tokens
|
|
907
1005
|
- Create or update users in your database
|
|
908
|
-
-
|
|
909
|
-
- Store tokens securely
|
|
1006
|
+
- Store tokens securely in your database
|
|
910
1007
|
- Add custom claims or metadata
|
|
1008
|
+
- Implement custom session storage (Redis, database, etc.)
|
|
911
1009
|
|
|
912
|
-
**Example with database integration
|
|
1010
|
+
**Example with database integration:**
|
|
913
1011
|
```typescript
|
|
914
|
-
import {
|
|
1012
|
+
import { SessionHandler, OAuthTokenResponse, Session, extractUserInfo } from "@vunexa/lixa";
|
|
915
1013
|
|
|
916
1014
|
interface CustomSessionData extends OAuthTokenResponse {
|
|
917
1015
|
userId: string;
|
|
918
1016
|
}
|
|
919
1017
|
|
|
920
|
-
const
|
|
921
|
-
|
|
922
|
-
//
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
// Decode ID token (for OIDC providers)
|
|
926
|
-
const payload = tokenData.id_token
|
|
927
|
-
? decodeJwt(tokenData.id_token)
|
|
928
|
-
: null;
|
|
1018
|
+
const dbSessionHandler: SessionHandler = {
|
|
1019
|
+
generateSession: async (tokenData: OAuthTokenResponse, providerMetadata): Promise<Session<CustomSessionData>> => {
|
|
1020
|
+
// Extract user info using lixa's utility
|
|
1021
|
+
const { userInfo } = await extractUserInfo(tokenData, providerMetadata);
|
|
929
1022
|
|
|
930
1023
|
// Create/update user
|
|
931
1024
|
const user = await db.users.upsert({
|
|
932
|
-
email:
|
|
933
|
-
name:
|
|
934
|
-
});
|
|
935
|
-
|
|
936
|
-
// Generate session
|
|
937
|
-
const sessionId = generateId();
|
|
938
|
-
await db.sessions.create({
|
|
939
|
-
id: sessionId,
|
|
940
|
-
userId: user.id,
|
|
941
|
-
accessToken: tokenData.access_token,
|
|
942
|
-
refreshToken: tokenData.refresh_token,
|
|
943
|
-
expiresAt: tokenData.expires_in
|
|
944
|
-
? new Date(Date.now() + tokenData.expires_in * 1000)
|
|
945
|
-
: null
|
|
1025
|
+
email: userInfo.email,
|
|
1026
|
+
name: userInfo.name
|
|
946
1027
|
});
|
|
947
1028
|
|
|
948
1029
|
return {
|
|
949
|
-
token:
|
|
1030
|
+
token: tokenData.access_token,
|
|
950
1031
|
raw: {
|
|
951
1032
|
...tokenData,
|
|
952
1033
|
userId: user.id
|
|
953
1034
|
}
|
|
954
1035
|
};
|
|
955
|
-
}
|
|
1036
|
+
},
|
|
1037
|
+
|
|
1038
|
+
sessionStorage: {
|
|
1039
|
+
saveSession: async (sessionId, session, ttl) => {
|
|
1040
|
+
await db.sessions.create({
|
|
1041
|
+
id: sessionId,
|
|
1042
|
+
userId: session.raw.userId,
|
|
1043
|
+
accessToken: session.raw.access_token,
|
|
1044
|
+
refreshToken: session.raw.refresh_token,
|
|
1045
|
+
expiresAt: new Date(Date.now() + ttl * 1000),
|
|
1046
|
+
});
|
|
1047
|
+
},
|
|
1048
|
+
getSession: async (sessionId) => {
|
|
1049
|
+
const session = await db.sessions.findOne({ id: sessionId });
|
|
1050
|
+
return session ? { token: session.accessToken, raw: session } : null;
|
|
1051
|
+
},
|
|
1052
|
+
deleteSession: async (sessionId) => {
|
|
1053
|
+
await db.sessions.delete({ id: sessionId });
|
|
1054
|
+
},
|
|
1055
|
+
},
|
|
956
1056
|
};
|
|
957
1057
|
```
|
|
958
1058
|
|
|
959
|
-
###
|
|
1059
|
+
### StateHandler Interface
|
|
960
1060
|
|
|
961
|
-
The `
|
|
1061
|
+
The `StateHandler` interface manages OAuth state generation and storage for CSRF protection and PKCE:
|
|
962
1062
|
|
|
963
1063
|
```typescript
|
|
964
|
-
interface
|
|
1064
|
+
interface StateHandler {
|
|
1065
|
+
generateState?(provider: string): Promise<{ state: string; data: StateData }>;
|
|
1066
|
+
stateStorage?: StateStorage;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
interface StateStorage {
|
|
965
1070
|
saveState(state: string, data: StateData, expiresInSeconds: number): Promise<void>;
|
|
966
1071
|
getState(state: string): Promise<StateData | null>;
|
|
967
1072
|
deleteState(state: string): Promise<void>;
|
|
@@ -974,60 +1079,62 @@ interface StateData {
|
|
|
974
1079
|
}
|
|
975
1080
|
```
|
|
976
1081
|
|
|
977
|
-
**
|
|
1082
|
+
**StateData fields:**
|
|
978
1083
|
- `provider`: Used to route callbacks to the correct provider configuration
|
|
979
1084
|
- `codeVerifier`: Required for PKCE token exchange (RFC 7636)
|
|
980
1085
|
- `createdAt`: Timestamp for debugging and validation
|
|
981
1086
|
|
|
982
1087
|
**Example with Redis:**
|
|
983
1088
|
```typescript
|
|
984
|
-
const
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1089
|
+
const redisStateHandler: StateHandler = {
|
|
1090
|
+
stateStorage: {
|
|
1091
|
+
saveState: async (state, data, expiresInSeconds) => {
|
|
1092
|
+
await redis.setex(
|
|
1093
|
+
`oauth:state:${state}`,
|
|
1094
|
+
expiresInSeconds,
|
|
1095
|
+
JSON.stringify(data)
|
|
1096
|
+
);
|
|
1097
|
+
},
|
|
1098
|
+
|
|
1099
|
+
getState: async (state) => {
|
|
1100
|
+
const data = await redis.get(`oauth:state:${state}`);
|
|
1101
|
+
return data ? JSON.parse(data) : null;
|
|
1102
|
+
},
|
|
1103
|
+
|
|
1104
|
+
deleteState: async (state) => {
|
|
1105
|
+
await redis.del(`oauth:state:${state}`);
|
|
1106
|
+
}
|
|
1000
1107
|
}
|
|
1001
1108
|
};
|
|
1002
1109
|
```
|
|
1003
1110
|
|
|
1004
|
-
###
|
|
1111
|
+
### SessionStorage Interface
|
|
1005
1112
|
|
|
1006
|
-
The `
|
|
1113
|
+
The `SessionStorage` interface manages persistent user session storage:
|
|
1007
1114
|
|
|
1008
1115
|
```typescript
|
|
1009
|
-
interface
|
|
1010
|
-
saveSession<T
|
|
1011
|
-
getSession<T
|
|
1116
|
+
interface SessionStorage {
|
|
1117
|
+
saveSession<T extends Session>(sessionId: string, session: T, expiresInSeconds: number): Promise<void>;
|
|
1118
|
+
getSession<T extends Session>(sessionId: string): Promise<T | null>;
|
|
1012
1119
|
deleteSession(sessionId: string): Promise<void>;
|
|
1013
1120
|
}
|
|
1014
1121
|
```
|
|
1015
1122
|
|
|
1016
|
-
**Example with database
|
|
1123
|
+
**Example with database:**
|
|
1017
1124
|
```typescript
|
|
1018
|
-
import {
|
|
1125
|
+
import { SessionStorage, Session } from "@vunexa/lixa";
|
|
1019
1126
|
|
|
1020
|
-
const
|
|
1021
|
-
saveSession: async <T
|
|
1127
|
+
const dbSessionStorage: SessionStorage = {
|
|
1128
|
+
saveSession: async <T extends Session>(sessionId: string, session: T, expiresInSeconds: number) => {
|
|
1022
1129
|
const expiresAt = new Date(Date.now() + expiresInSeconds * 1000);
|
|
1023
1130
|
await db.sessions.create({
|
|
1024
1131
|
id: sessionId,
|
|
1025
|
-
data: JSON.stringify(
|
|
1132
|
+
data: JSON.stringify(session),
|
|
1026
1133
|
expiresAt
|
|
1027
1134
|
});
|
|
1028
1135
|
},
|
|
1029
1136
|
|
|
1030
|
-
getSession: async <T
|
|
1137
|
+
getSession: async <T extends Session>(sessionId: string): Promise<T | null> => {
|
|
1031
1138
|
const session = await db.sessions.findOne({
|
|
1032
1139
|
id: sessionId,
|
|
1033
1140
|
expiresAt: { $gt: new Date() }
|
|
@@ -1084,65 +1191,6 @@ Debug output includes:
|
|
|
1084
1191
|
- `[Lixa] [timestamp] [INFO] [State]` - State storage with code verifier
|
|
1085
1192
|
- `[Lixa] [timestamp] [INFO] [Token]` - Token exchange with code verifier
|
|
1086
1193
|
|
|
1087
|
-
## Migration Guide
|
|
1088
|
-
|
|
1089
|
-
### Migrating from Legacy Registration Pattern
|
|
1090
|
-
|
|
1091
|
-
**Old Pattern (Deprecated):**
|
|
1092
|
-
```typescript
|
|
1093
|
-
import { Lixa, IProvider } from "@vunexa/lixa";
|
|
1094
|
-
import { GoogleProvider } from "@vunexa/lixa";
|
|
1095
|
-
|
|
1096
|
-
// Step 1: Register providers globally
|
|
1097
|
-
Lixa.registerProvider({
|
|
1098
|
-
google: new GoogleProvider()
|
|
1099
|
-
});
|
|
1100
|
-
|
|
1101
|
-
// Step 2: Configure with credentials only
|
|
1102
|
-
const lixa = new Lixa({
|
|
1103
|
-
providers: {
|
|
1104
|
-
google: {
|
|
1105
|
-
clientId: "...",
|
|
1106
|
-
clientSecret: "...",
|
|
1107
|
-
redirectUri: "...",
|
|
1108
|
-
scopes: ["openid", "email"]
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
});
|
|
1112
|
-
```
|
|
1113
|
-
|
|
1114
|
-
**New Pattern (Recommended):**
|
|
1115
|
-
```typescript
|
|
1116
|
-
import { Lixa } from "@vunexa/lixa";
|
|
1117
|
-
import { GoogleProvider } from "@vunexa/lixa-providers";
|
|
1118
|
-
|
|
1119
|
-
// Single step: Pass provider inline
|
|
1120
|
-
const lixa = new Lixa({
|
|
1121
|
-
providers: {
|
|
1122
|
-
google: {
|
|
1123
|
-
provider: new GoogleProvider(), // ← Add this line
|
|
1124
|
-
clientId: "...",
|
|
1125
|
-
clientSecret: "...",
|
|
1126
|
-
redirectUri: "...",
|
|
1127
|
-
scopes: ["openid", "email"]
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
});
|
|
1131
|
-
```
|
|
1132
|
-
|
|
1133
|
-
**Migration Steps:**
|
|
1134
|
-
1. Install `@vunexa/lixa-providers`: `npm install @vunexa/lixa-providers`
|
|
1135
|
-
2. Update imports: `import { GoogleProvider } from "@vunexa/lixa-providers"`
|
|
1136
|
-
3. Remove `Lixa.registerProvider()` calls
|
|
1137
|
-
4. Add `provider` field to each provider configuration
|
|
1138
|
-
5. Test your OAuth flows
|
|
1139
|
-
|
|
1140
|
-
**Benefits:**
|
|
1141
|
-
- ✅ No global state - each Lixa instance is independent
|
|
1142
|
-
- ✅ Better testability - easy to mock providers per instance
|
|
1143
|
-
- ✅ Clearer configuration - everything in one place
|
|
1144
|
-
- ✅ Type safety - TypeScript knows which providers are configured
|
|
1145
|
-
|
|
1146
1194
|
## Security Considerations
|
|
1147
1195
|
|
|
1148
1196
|
### PKCE (Proof Key for Code Exchange)
|