@vunexa/lixa 0.0.1-alpha.3 → 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.
Files changed (48) hide show
  1. package/README.md +1081 -76
  2. package/dist/dao/session-cache.d.ts +10 -0
  3. package/dist/dao/session-cache.d.ts.map +1 -0
  4. package/dist/dao/state-cache.d.ts +12 -0
  5. package/dist/dao/state-cache.d.ts.map +1 -0
  6. package/dist/dao/types.d.ts +370 -0
  7. package/dist/dao/types.d.ts.map +1 -0
  8. package/dist/export-types/index.d.ts +1220 -0
  9. package/dist/export-types/tsdoc-metadata.json +11 -0
  10. package/dist/index.cjs +723 -0
  11. package/dist/index.cjs.map +1 -0
  12. package/dist/index.d.cts +1189 -0
  13. package/dist/index.d.ts +19 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +681 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/lixa.d.ts +322 -11
  18. package/dist/lixa.d.ts.map +1 -1
  19. package/dist/models/session.d.ts +273 -0
  20. package/dist/models/session.d.ts.map +1 -0
  21. package/dist/providers/IProvider.d.ts +128 -0
  22. package/dist/providers/IProvider.d.ts.map +1 -1
  23. package/dist/providers/index.d.ts +0 -2
  24. package/dist/providers/index.d.ts.map +1 -1
  25. package/dist/types.d.ts +167 -16
  26. package/dist/types.d.ts.map +1 -1
  27. package/dist/utils/user-info.d.ts +82 -0
  28. package/dist/utils/user-info.d.ts.map +1 -0
  29. package/package.json +20 -10
  30. package/dist/lixa.js +0 -107
  31. package/dist/lixa.js.map +0 -1
  32. package/dist/providers/IProvider.js +0 -3
  33. package/dist/providers/IProvider.js.map +0 -1
  34. package/dist/providers/github.d.ts +0 -9
  35. package/dist/providers/github.d.ts.map +0 -1
  36. package/dist/providers/github.js +0 -8
  37. package/dist/providers/github.js.map +0 -1
  38. package/dist/providers/google.d.ts +0 -9
  39. package/dist/providers/google.d.ts.map +0 -1
  40. package/dist/providers/google.js +0 -8
  41. package/dist/providers/google.js.map +0 -1
  42. package/dist/providers/index.js +0 -3
  43. package/dist/providers/index.js.map +0 -1
  44. package/dist/types.js +0 -2
  45. package/dist/types.js.map +0 -1
  46. package/dist/utils/constants.js +0 -4
  47. package/dist/utils/constants.js.map +0 -1
  48. package/index.d.ts +0 -63
package/README.md CHANGED
@@ -13,13 +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 support for popular providers like Google and GitHub
18
- - Custom provider registration with extensible provider interface
19
- - Extensible session management via pluggable strategies
20
- - PKCE (Proof Key for Code Exchange) support
21
- - TypeScript-first with strong typing and async/await support
22
- - 100% test coverage with comprehensive error handling
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
23
26
 
24
27
  ---
25
28
 
@@ -33,61 +36,81 @@ yarn add @vunexa/lixa
33
36
 
34
37
  ## Quick Start
35
38
 
36
- ### 1. Configure lixa with multiple providers
39
+ ### 1. Install packages
40
+
41
+ ```bash
42
+ npm install @vunexa/lixa @vunexa/lixa-providers
43
+ ```
44
+
45
+ ### 2. Configure Lixa with providers
37
46
 
38
47
  ```typescript
39
- import { Lixa } from '@vunexa/lixa';
48
+ import { Lixa } from "@vunexa/lixa";
49
+ import { GoogleProvider, GithubProvider } from "@vunexa/lixa-providers";
40
50
 
41
51
  const lixa = new Lixa({
42
52
  providers: {
43
53
  google: {
54
+ provider: new GoogleProvider(),
44
55
  clientId: process.env.GOOGLE_CLIENT_ID!,
45
56
  clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
46
- redirectUri: 'https://yourapp.com/auth/google/callback',
47
- scopes: ['openid', 'email', 'profile'],
57
+ redirectUri: "https://yourapp.com/auth/google/callback",
58
+ scopes: ["openid", "email", "profile"],
48
59
  extraConfig: {
49
- prompt: 'consent',
50
- access_type: 'offline',
60
+ prompt: "consent",
61
+ access_type: "offline",
51
62
  },
52
63
  },
53
64
  github: {
65
+ provider: new GithubProvider(),
54
66
  clientId: process.env.GITHUB_CLIENT_ID!,
55
67
  clientSecret: process.env.GITHUB_CLIENT_SECRET!,
56
- redirectUri: 'https://yourapp.com/auth/github/callback',
57
- scopes: ['read:user', 'user:email'],
58
- extraConfig: {},
68
+ redirectUri: "https://yourapp.com/auth/github/callback",
69
+ scopes: ["read:user", "user:email"],
59
70
  },
60
71
  },
61
72
 
62
- // Optional: custom session strategy
63
- sessionStrategy: {
64
- createSession: async (tokenData) => {
65
- // Custom session creation logic
73
+ // Optional: custom session handler
74
+ sessionHandler: {
75
+ // Optional: customize session generation
76
+ generateSession: async (tokenData, providerMetadata) => {
77
+ // tokenData is typed as OAuthTokenResponse with proper OAuth 2.0 fields
78
+ // { access_token, token_type, expires_in?, refresh_token?, scope?, id_token? }
66
79
  return {
67
- token: 'custom-session-token',
68
- raw: tokenData
80
+ token: tokenData.access_token,
81
+ raw: tokenData,
69
82
  };
70
- }
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
+ },
71
97
  },
72
98
  });
73
99
  ```
74
100
 
75
- ### 2. Redirect users to the provider's authorization URL
101
+ ### 3. Redirect users to the provider's authorization URL
76
102
 
77
103
  ```typescript
78
- app.get("/login", (req, res) => {
104
+ app.get("/login", async (req, res) => {
79
105
  const provider = req.query.provider as string; // 'google' or 'github'
80
- const state = lixa.generateRandomState();
81
-
82
- // Store state in session for validation
83
- req.session.oauthState = state;
84
-
85
- const authUrl = lixa.getAuthUrl(provider.toUpperCase(), state);
106
+
107
+ // getAuthUrl automatically generates state if not provided
108
+ const authUrl = await lixa.getAuthUrl(provider);
86
109
  res.redirect(authUrl);
87
110
  });
88
111
  ```
89
112
 
90
- ### 3. Handle the provider callback and establish a session
113
+ ### 4. Handle the provider callback and establish a session
91
114
 
92
115
  ```typescript
93
116
  app.get("/auth/:provider/callback", async (req, res) => {
@@ -95,24 +118,23 @@ app.get("/auth/:provider/callback", async (req, res) => {
95
118
  const provider = req.params.provider;
96
119
 
97
120
  try {
98
- // Validate state parameter
99
- if (state !== req.session.oauthState) {
100
- throw new Error('Invalid state parameter');
101
- }
102
-
103
- const session = await lixa.handleCallback({
121
+ // State validation is handled automatically by lixa
122
+ const sessionId = await lixa.handleCallback({
104
123
  provider,
105
124
  code: code as string,
106
125
  state: state as string,
107
126
  });
108
127
 
109
- // Session established
110
- res.cookie("session_token", session.token, {
128
+ // Session established - retrieve session info if needed
129
+ const session = await lixa.fetchSessionInfo(sessionId);
130
+
131
+ // Store session ID in cookie
132
+ res.cookie("session_id", sessionId, {
111
133
  httpOnly: true,
112
134
  secure: true,
113
- sameSite: 'strict'
135
+ sameSite: "strict",
114
136
  });
115
-
137
+
116
138
  res.redirect("/dashboard");
117
139
  } catch (error) {
118
140
  console.error("Authentication error:", error);
@@ -121,87 +143,758 @@ app.get("/auth/:provider/callback", async (req, res) => {
121
143
  });
122
144
  ```
123
145
 
146
+ ## Provider Configuration
147
+
148
+ ### Using Built-in Providers
149
+
150
+ Built-in providers are available in the `@vunexa/lixa-providers` package:
151
+
152
+ ```typescript
153
+ import { Lixa } from "@vunexa/lixa";
154
+ import { GoogleProvider, GithubProvider } from "@vunexa/lixa-providers";
155
+
156
+ const lixa = new Lixa({
157
+ providers: {
158
+ google: {
159
+ provider: new GoogleProvider(),
160
+ clientId: process.env.GOOGLE_CLIENT_ID!,
161
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
162
+ redirectUri: "https://yourapp.com/auth/google/callback",
163
+ scopes: ["openid", "email", "profile"]
164
+ }
165
+ }
166
+ });
167
+ ```
168
+
169
+ ### Using Custom Inline Providers
170
+
171
+ You can create custom providers by implementing the `IProvider` interface:
172
+
173
+ ```typescript
174
+ import { Lixa, IProvider } from "@vunexa/lixa";
175
+
176
+ // Define your custom provider
177
+ const customProvider: IProvider = {
178
+ authorizationEndpoint: "https://provider.com/oauth/authorize",
179
+ tokenEndpoint: "https://provider.com/oauth/token",
180
+ userInfoEndpoint: "https://provider.com/api/user"
181
+ };
182
+
183
+ // Use it directly in configuration
184
+ const lixa = new Lixa({
185
+ providers: {
186
+ custom: {
187
+ provider: customProvider,
188
+ clientId: "your-client-id",
189
+ clientSecret: "your-client-secret",
190
+ redirectUri: "https://yourapp.com/auth/custom/callback",
191
+ scopes: ["read:user"]
192
+ }
193
+ }
194
+ });
195
+ ```
196
+
197
+ ### Mixing Built-in and Custom Providers
198
+
199
+ You can use both built-in and custom providers in the same configuration:
200
+
201
+ ```typescript
202
+ import { Lixa, IProvider } from "@vunexa/lixa";
203
+ import { GoogleProvider } from "@vunexa/lixa-providers";
204
+
205
+ const customProvider: IProvider = {
206
+ authorizationEndpoint: "https://custom.com/oauth/authorize",
207
+ tokenEndpoint: "https://custom.com/oauth/token",
208
+ userInfoEndpoint: "https://custom.com/api/user"
209
+ };
210
+
211
+ const lixa = new Lixa({
212
+ providers: {
213
+ google: {
214
+ provider: new GoogleProvider(),
215
+ clientId: process.env.GOOGLE_CLIENT_ID!,
216
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
217
+ redirectUri: "https://yourapp.com/auth/google/callback",
218
+ scopes: ["openid", "email", "profile"]
219
+ },
220
+ custom: {
221
+ provider: customProvider,
222
+ clientId: process.env.CUSTOM_CLIENT_ID!,
223
+ clientSecret: process.env.CUSTOM_CLIENT_SECRET!,
224
+ redirectUri: "https://yourapp.com/auth/custom/callback",
225
+ scopes: ["read:user"]
226
+ }
227
+ }
228
+ });
229
+ ```
230
+
231
+ ### Overriding Built-in Providers (Testing)
232
+
233
+ You can override built-in providers with custom implementations for testing:
234
+
235
+ ```typescript
236
+ import { Lixa, IProvider } from "@vunexa/lixa";
237
+
238
+ // Mock provider for testing
239
+ const mockGoogleProvider: IProvider = {
240
+ authorizationEndpoint: "http://localhost:3000/mock/authorize",
241
+ tokenEndpoint: "http://localhost:3000/mock/token",
242
+ userInfoEndpoint: "http://localhost:3000/mock/userinfo"
243
+ };
244
+
245
+ const lixa = new Lixa({
246
+ providers: {
247
+ google: {
248
+ provider: mockGoogleProvider, // Override with mock
249
+ clientId: "test-client-id",
250
+ clientSecret: "test-client-secret",
251
+ redirectUri: "http://localhost:3000/callback",
252
+ scopes: ["openid", "email"]
253
+ }
254
+ }
255
+ });
256
+ ```
257
+
124
258
  ## Advanced Usage
125
259
 
126
- ### Custom Provider Registration
260
+ ### Custom State Handler
127
261
 
128
- You can register custom OAuth providers by implementing the `IProvider` interface:
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:
129
263
 
130
264
  ```typescript
131
- import { Lixa, IProvider } from '@vunexa/lixa';
265
+ import { Lixa, StateHandler, StateStorage, StateData } from "@vunexa/lixa";
266
+
267
+ // Example: DynamoDB state storage
268
+ const dynamoDbStateStorage: StateStorage = {
269
+ saveState: async (state: string, data: StateData, expiresInSeconds: number) => {
270
+ await dynamoDB.put({
271
+ TableName: "oauth-states",
272
+ Item: {
273
+ state,
274
+ data: JSON.stringify(data),
275
+ expiresAt: Math.floor(Date.now() / 1000) + expiresInSeconds,
276
+ },
277
+ });
278
+ },
279
+
280
+ getState: async (state: string): Promise<StateData | null> => {
281
+ const result = await dynamoDB.get({
282
+ TableName: "oauth-states",
283
+ Key: { state },
284
+ });
285
+
286
+ if (!result.Item) return null;
287
+
288
+ if (result.Item.expiresAt < Math.floor(Date.now() / 1000)) {
289
+ return null; // Expired
290
+ }
291
+
292
+ return JSON.parse(result.Item.data) as StateData;
293
+ },
294
+
295
+ deleteState: async (state: string) => {
296
+ await dynamoDB.delete({
297
+ TableName: "oauth-states",
298
+ Key: { state },
299
+ });
300
+ },
301
+ };
302
+
303
+ const lixa = new Lixa({
304
+ providers: { /* ... */ },
305
+ stateHandler: {
306
+ stateStorage: dynamoDbStateStorage,
307
+ // Optional: customize state generation
308
+ // generateState: async (provider) => { ... }
309
+ },
310
+ });
311
+ ```
312
+
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:**
318
+ - `provider` (string): Provider name for callback routing
319
+ - `codeVerifier` (string): PKCE code verifier (64 hex characters)
320
+ - `createdAt` (number): Unix timestamp in milliseconds
321
+
322
+ ### Custom Session Handler
323
+
324
+ The session handler manages both session generation and storage. You can customize either or both:
325
+
326
+ ```typescript
327
+ import { Lixa, SessionHandler, SessionStorage, extractUserInfo } from "@vunexa/lixa";
328
+
329
+ const customSessionStorage: SessionStorage = {
330
+ saveSession: async <T = unknown>(sessionId: string, session: T, expiresInSeconds: number) => {
331
+ await db.sessions.create({
332
+ id: sessionId,
333
+ data: session,
334
+ expiresAt: Date.now() + expiresInSeconds * 1000,
335
+ });
336
+ },
337
+
338
+ getSession: async <T = unknown>(sessionId: string): Promise<T | null> => {
339
+ const session = await db.sessions.findById(sessionId);
340
+ if (!session || session.expiresAt < Date.now()) {
341
+ return null;
342
+ }
343
+ return session.data as T;
344
+ },
345
+
346
+ deleteSession: async (sessionId: string) => {
347
+ await db.sessions.delete(sessionId);
348
+ },
349
+ };
350
+
351
+ const lixa = new Lixa({
352
+ providers: { /* ... */ },
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
+ },
373
+ });
374
+ ```
375
+
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
381
+
382
+ The `generateSession` method controls how user sessions are created from OAuth tokens:
383
+
384
+ ```typescript
385
+ import { Lixa, SessionHandler, OAuthTokenResponse, Session, extractUserInfo } from "@vunexa/lixa";
386
+
387
+ // Define custom session data structure
388
+ interface CustomSessionData extends OAuthTokenResponse {
389
+ userId: string;
390
+ userInfo: {
391
+ email: string;
392
+ name: string;
393
+ };
394
+ }
395
+
396
+ const customSessionHandler: SessionHandler = {
397
+ generateSession: async (tokenData: OAuthTokenResponse, providerMetadata): Promise<Session<CustomSessionData>> => {
398
+ // tokenData is properly typed with OAuth 2.0 fields:
399
+ // - access_token: string (required)
400
+ // - token_type: string (required)
401
+ // - expires_in?: number
402
+ // - refresh_token?: string
403
+ // - scope?: string
404
+ // - id_token?: string (for OIDC providers)
405
+
406
+ // Extract user info using lixa's utility
407
+ const { userInfo } = await extractUserInfo(tokenData, providerMetadata);
408
+
409
+ // Create or update user in your database
410
+ const user = await db.users.upsert({
411
+ email: userInfo.email,
412
+ name: userInfo.name,
413
+ // ... other fields
414
+ });
415
+
416
+ return {
417
+ token: tokenData.access_token,
418
+ raw: {
419
+ ...tokenData,
420
+ userId: user.id,
421
+ userInfo,
422
+ },
423
+ };
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
+ },
439
+ };
440
+
441
+ const lixa = new Lixa({
442
+ providers: { /* ... */ },
443
+ sessionHandler: customSessionHandler,
444
+ });
445
+ ```
446
+
447
+ ### Debug Mode
448
+
449
+ Enable debug logging to troubleshoot OAuth flows:
450
+
451
+ ```typescript
452
+ const lixa = new Lixa({
453
+ providers: { /* ... */ },
454
+ debug: true, // Enables detailed logging
455
+ });
456
+ ```
457
+
458
+ Debug mode logs:
459
+ - Token exchange requests and responses
460
+ - State validation
461
+ - Session creation
462
+ - Error details from OAuth providers
463
+
464
+ ### Legacy Provider Registration (Deprecated)
465
+
466
+ > **Note**: This pattern is deprecated. Use inline providers instead (see examples above).
467
+
468
+ For backward compatibility, you can still register providers globally:
469
+
470
+ ```typescript
471
+ import { Lixa, IProvider } from "@vunexa/lixa";
132
472
 
133
473
  class CustomProvider implements IProvider {
134
- authorizationEndpoint = 'https://custom-provider.com/oauth/authorize';
135
- tokenEndpoint = 'https://custom-provider.com/oauth/token';
136
- userInfoEndpoint = 'https://custom-provider.com/api/user';
474
+ authorizationEndpoint = "https://custom-provider.com/oauth/authorize";
475
+ tokenEndpoint = "https://custom-provider.com/oauth/token";
476
+ userInfoEndpoint = "https://custom-provider.com/api/user";
137
477
  }
138
478
 
139
- // Register the custom provider
479
+ // Register the custom provider BEFORE creating Lixa instance
140
480
  Lixa.registerProvider({
141
- custom: new CustomProvider()
481
+ custom: new CustomProvider(),
142
482
  });
143
483
 
144
- // Use it in your configuration
484
+ // Use it in your configuration (without provider field)
145
485
  const lixa = new Lixa({
146
486
  providers: {
147
487
  custom: {
148
- clientId: 'your-client-id',
149
- clientSecret: 'your-client-secret',
150
- redirectUri: 'https://yourapp.com/auth/custom/callback',
151
- scopes: ['read:user'],
152
- }
488
+ clientId: "your-client-id",
489
+ clientSecret: "your-client-secret",
490
+ redirectUri: "https://yourapp.com/auth/custom/callback",
491
+ scopes: ["read:user"],
492
+ },
493
+ },
494
+ });
495
+ ```
496
+
497
+ ### Check Provider Configuration
498
+
499
+ ```typescript
500
+ // Check if a provider is configured for this instance
501
+ if (lixa.isProviderConfigured("google")) {
502
+ console.log("Google provider is configured");
503
+ }
504
+
505
+ // Get list of all registered providers (legacy)
506
+ const providers = Lixa.getRegisteredProviders();
507
+ console.log("Registered providers:", providers);
508
+ ```
509
+
510
+ ## Common Pitfalls & Solutions
511
+
512
+ ### 1. "Invalid or expired state" Error
513
+
514
+ **Cause**: The state parameter is not being stored or retrieved correctly.
515
+
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
+ ```
530
+
531
+ ### 2. "Missing code verifier" Error
532
+
533
+ **Cause**: The `codeVerifier` field is not being preserved in your state storage.
534
+
535
+ **Solution**: Ensure your `stateStorage.saveState()` stores ALL fields from the data object, including `codeVerifier`:
536
+
537
+ ```typescript
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: ... });
153
545
  }
546
+ }
547
+ ```
548
+
549
+ ### 3. Token Exchange Fails with 400 Bad Request
550
+
551
+ **Causes**:
552
+ - Redirect URI mismatch between your config and OAuth provider console
553
+ - Invalid client ID or secret
554
+ - Code has already been used or expired
555
+
556
+ **Solution**:
557
+ - Enable debug mode to see the exact error from the provider
558
+ - Verify redirect URI matches exactly (including protocol, port, path)
559
+ - Check that client credentials are correct
560
+
561
+ ### 4. Session Not Found After Creation
562
+
563
+ **Cause**: Using default in-memory session storage in serverless/distributed environments.
564
+
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
+ },
154
577
  });
155
578
  ```
156
579
 
157
- ### Check Provider Registration
580
+ ### 5. User Info is Undefined
581
+
582
+ **Cause**: The `generateSession()` receives raw token data, not user info.
583
+
584
+ **Solution**: Use lixa's `extractUserInfo()` utility to automatically extract user info:
158
585
 
159
586
  ```typescript
160
- // Check if a provider is registered
161
- if (Lixa.isProviderRegistered('google')) {
162
- console.log('Google provider is available');
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);
593
+
594
+ // Now you have user info
595
+ console.log(userInfo.email, userInfo.name);
596
+
597
+ return {
598
+ token: tokenData.access_token,
599
+ raw: tokenData
600
+ };
601
+ }
602
+ };
603
+ ```
604
+
605
+ ## User Info Utilities
606
+
607
+ Lixa provides utilities to extract user information from OAuth tokens, making it easier to work with user data across different providers.
608
+
609
+ ### `extractUserInfo()`
610
+
611
+ High-level function that automatically extracts user information from OAuth token data:
612
+
613
+ ```typescript
614
+ import { extractUserInfo, type UserInfo } from "@vunexa/lixa";
615
+
616
+ // In your session handler
617
+ const sessionHandler = {
618
+ generateSession: async (tokenData, providerMetadata) => {
619
+ // Automatically extracts user info from ID token or fetches from API
620
+ const { userInfo } = await extractUserInfo(tokenData, providerMetadata);
621
+
622
+ console.log(`User ${userInfo.email} authenticated`);
623
+
624
+ // Create user in database
625
+ const user = await db.users.upsert({
626
+ email: userInfo.email,
627
+ name: userInfo.name,
628
+ givenName: userInfo.given_name,
629
+ familyName: userInfo.family_name,
630
+ picture: userInfo.picture,
631
+ });
632
+
633
+ return {
634
+ token: generateSessionId(),
635
+ raw: { ...tokenData, userId: user.id }
636
+ };
637
+ }
638
+ };
639
+ ```
640
+
641
+ **Parameters:**
642
+ - `tokenData` (OAuthTokenResponse) - OAuth token response from provider
643
+ - `providerMetadata` (ProviderMetadata) - Provider metadata containing endpoints configuration
644
+
645
+ **Returns:**
646
+ - `{ userInfo: UserInfo }` - User information extracted from token or fetched from provider
647
+
648
+ **Behavior:**
649
+ 1. If ID token present: Decodes JWT and extracts user info (OIDC providers like Google)
650
+ 2. If only access token: Fetches from userinfo endpoint using `providerMetadata.endpoints.userInfo` (OAuth providers like GitHub)
651
+ 3. Automatically determines the best method based on available token data
652
+ 4. Throws clear errors if neither ID token nor access token is available
653
+
654
+ ### `decodeIdToken()`
655
+
656
+ Decode JWT ID tokens to extract user information:
657
+
658
+ ```typescript
659
+ import { decodeIdToken, type UserInfo } from "@vunexa/lixa";
660
+
661
+ const userInfo: UserInfo = decodeIdToken(tokenData.id_token);
662
+ console.log(userInfo.email, userInfo.name);
663
+ ```
664
+
665
+ **Use case:** When you know the provider returns an ID token (OIDC providers like Google)
666
+
667
+ ### `fetchUserInfo()`
668
+
669
+ Fetch user information from a provider's userinfo endpoint:
670
+
671
+ ```typescript
672
+ import { fetchUserInfo, type UserInfo } from "@vunexa/lixa";
673
+
674
+ const userInfo: UserInfo = await fetchUserInfo(
675
+ tokenData.access_token,
676
+ 'https://www.googleapis.com/oauth2/v2/userinfo',
677
+ 'google' // Optional: for error messages
678
+ );
679
+ ```
680
+
681
+ **Use case:** When you need to fetch user info from the API (OAuth-only providers like GitHub, or when ID token is not available)
682
+
683
+ ### `determineProviderFromIssuer()`
684
+
685
+ Detect OAuth provider from ID token issuer field:
686
+
687
+ ```typescript
688
+ import { determineProviderFromIssuer, decodeIdToken } from "@vunexa/lixa";
689
+
690
+ const userInfo = decodeIdToken(tokenData.id_token);
691
+ const provider = determineProviderFromIssuer(userInfo);
692
+ // Returns: 'google', 'github', or null if unknown
693
+ ```
694
+
695
+ **Supported providers:**
696
+ - Google: Detects `accounts.google.com` in issuer
697
+ - GitHub: Detects `github` in issuer
698
+ - Returns `null` for unknown issuers
699
+
700
+ **Note:** This utility is primarily for informational purposes. The `extractUserInfo()` function no longer returns provider information as it now relies on `providerMetadata` parameter.
701
+
702
+ ### `UserInfo` Type
703
+
704
+ Standard user information structure across all providers:
705
+
706
+ ```typescript
707
+ interface UserInfo {
708
+ email: string; // Required: User's email address
709
+ id?: string; // Optional: Provider-specific user ID
710
+ sub?: string; // Optional: Subject identifier (OIDC)
711
+ given_name?: string; // Optional: First name
712
+ family_name?: string; // Optional: Last name
713
+ name?: string; // Optional: Full name
714
+ picture?: string; // Optional: Profile picture URL
715
+ email_verified?: boolean; // Optional: Email verification status
716
+ iss?: string; // Optional: Token issuer (OIDC)
163
717
  }
164
718
  ```
165
719
 
720
+ ### Complete Example with User Info Utilities
721
+
722
+ ```typescript
723
+ import { Lixa, extractUserInfo, type UserInfo } from "@vunexa/lixa";
724
+ import { GoogleProvider, GithubProvider } from "@vunexa/lixa-providers";
725
+
726
+ const lixa = new Lixa({
727
+ providers: {
728
+ google: {
729
+ provider: new GoogleProvider(),
730
+ clientId: process.env.GOOGLE_CLIENT_ID!,
731
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
732
+ redirectUri: "https://yourapp.com/auth/google/callback",
733
+ scopes: ["openid", "email", "profile"],
734
+ },
735
+ github: {
736
+ provider: new GithubProvider(),
737
+ clientId: process.env.GITHUB_CLIENT_ID!,
738
+ clientSecret: process.env.GITHUB_CLIENT_SECRET!,
739
+ redirectUri: "https://yourapp.com/auth/github/callback",
740
+ scopes: ["user:email"],
741
+ }
742
+ },
743
+
744
+ sessionHandler: {
745
+ generateSession: async (tokenData, providerMetadata) => {
746
+ // Extract user info - works for both Google (ID token) and GitHub (API fetch)
747
+ const { userInfo } = await extractUserInfo(tokenData, providerMetadata);
748
+
749
+ console.log(`User ${userInfo.email} authenticated`);
750
+
751
+ // Create or update user in database
752
+ const user = await db.users.upsert({
753
+ email: userInfo.email,
754
+ name: userInfo.name || `${userInfo.given_name} ${userInfo.family_name}`.trim(),
755
+ givenName: userInfo.given_name,
756
+ familyName: userInfo.family_name,
757
+ picture: userInfo.picture,
758
+ emailVerified: userInfo.email_verified,
759
+ });
760
+
761
+ return {
762
+ token: tokenData.access_token,
763
+ raw: {
764
+ ...tokenData,
765
+ userId: user.id,
766
+ }
767
+ };
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
+ },
787
+ }
788
+ });
789
+ ```
790
+
791
+ ### Benefits of User Info Utilities
792
+
793
+ ✅ **Provider-agnostic** - Works with any OAuth provider
794
+ ✅ **Automatic method selection** - Uses ID token or fetches from API based on what's available
795
+ ✅ **Type-safe** - Full TypeScript support with UserInfo interface
796
+ ✅ **Flexible** - Supports both OIDC (ID token) and OAuth-only (API fetch) providers
797
+ ✅ **Error handling** - Clear error messages when token data is invalid
798
+ ✅ **Uses provider metadata** - Leverages userInfoEndpoint from provider configuration
799
+
166
800
  ## API Reference
167
801
 
168
802
  ### `Lixa` Class
169
803
 
170
804
  #### Constructor
805
+
171
806
  - `new Lixa(config: LixaConfig)` - Creates a new Lixa instance
172
807
 
173
808
  #### Static Methods
174
- - `Lixa.registerProvider(providerMap: { [key: string]: IProvider })` - Register custom providers
175
- - `Lixa.isProviderRegistered(provider: string): boolean` - Check if a provider is registered
809
+
810
+ - `Lixa.registerProvider(providerMap: { [key: string]: IProvider })` - **[Deprecated]** Register custom providers globally. Use inline providers instead.
811
+ - `Lixa.getRegisteredProviders(): string[]` - **[Deprecated]** Get list of all registered provider names from legacy registry
176
812
 
177
813
  #### Instance Methods
178
- - `generateRandomState(): string` - Generate a random state parameter for OAuth flow
179
- - `getAuthUrl(provider: string, state: string): string` - Get authorization URL for a provider
180
- - `handleCallback({ provider, code, state }): Promise<Session>` - Handle OAuth callback and create session
814
+
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.
818
+ - `isProviderConfigured(provider: string): boolean` - Check if a provider is configured for this Lixa instance. Useful for type guards and runtime validation.
181
819
 
182
820
  ### Types
183
821
 
822
+ All types are fully typed with zero `any` types, following OAuth 2.0 and OpenID Connect specifications:
823
+
184
824
  ```typescript
185
- interface ProviderConfig {
825
+ // OAuth 2.0 token response (RFC 6749 Section 5.1)
826
+ interface OAuthTokenResponse {
827
+ access_token: string; // Required: OAuth access token
828
+ token_type: string; // Required: Token type (usually "Bearer")
829
+ expires_in?: number; // Optional: Token expiration in seconds
830
+ refresh_token?: string; // Optional: Refresh token
831
+ scope?: string; // Optional: Granted scopes (space-separated)
832
+ id_token?: string; // Optional: OpenID Connect ID token (JWT)
833
+ [key: string]: unknown; // Additional provider-specific fields
834
+ }
835
+
836
+ // Provider configuration with inline provider support
837
+ type ProviderConfig = {
186
838
  clientId: string;
187
839
  clientSecret: string;
188
840
  redirectUri: string;
189
841
  scopes: string[];
190
- extraConfig?: Record<string, any>;
842
+ extraConfig?: Record<string, string>;
843
+ } & (
844
+ | { provider?: never } // Built-in provider (no provider field)
845
+ | { provider: IProvider } // Custom provider (provider field required)
846
+ );
847
+
848
+ interface LixaConfig<TProviders = Record<string, ProviderConfig>> {
849
+ providers: TProviders;
850
+ sessionHandler?: SessionHandler;
851
+ stateHandler?: StateHandler;
852
+ debug?: boolean;
853
+ }
854
+
855
+ interface SessionHandler {
856
+ generateSession?<T extends Session>(
857
+ tokenData: OAuthTokenResponse,
858
+ providerMetadata: ProviderMetadata
859
+ ): Promise<T>;
860
+ sessionStorage?: SessionStorage;
191
861
  }
192
862
 
193
- interface LixaConfig {
194
- providers: Record<string, ProviderConfig>;
195
- sessionStrategy?: SessionStrategy;
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>;
196
867
  }
197
868
 
198
- interface SessionStrategy {
199
- createSession(userInfo: any): Promise<Session>;
869
+ interface StateHandler {
870
+ generateState?(provider: string): Promise<{ state: string; data: StateData }>;
871
+ stateStorage?: StateStorage;
200
872
  }
201
873
 
202
- interface Session {
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>;
878
+ }
879
+
880
+ interface Session<TRaw = OAuthTokenResponse> {
203
881
  token: string;
204
- raw: any;
882
+ raw: TRaw; // Defaults to OAuthTokenResponse, can be extended
883
+ }
884
+
885
+ interface StateData {
886
+ provider: string; // Provider name for routing
887
+ codeVerifier: string; // PKCE code verifier (64 hex chars)
888
+ createdAt: number; // Unix timestamp in milliseconds
889
+ }
890
+
891
+ interface ProviderMetadata {
892
+ name: string;
893
+ endpoints: {
894
+ authorization: string;
895
+ token: string;
896
+ userInfo: string;
897
+ };
205
898
  }
206
899
 
207
900
  interface IProvider {
@@ -213,8 +906,320 @@ interface IProvider {
213
906
 
214
907
  ## Built-in Providers
215
908
 
216
- - **Google** - OAuth 2.0 and OpenID Connect
217
- - **GitHub** - OAuth 2.0
909
+ Built-in providers are available in the `@vunexa/lixa-providers` package:
910
+
911
+ ```bash
912
+ npm install @vunexa/lixa-providers
913
+ ```
914
+
915
+ ### Google (`GoogleProvider`)
916
+
917
+ ```typescript
918
+ import { GoogleProvider } from "@vunexa/lixa-providers";
919
+ ```
920
+
921
+ - **Type**: OAuth 2.0 + OpenID Connect (OIDC)
922
+ - **Authorization Endpoint**: `https://accounts.google.com/o/oauth2/v2/auth`
923
+ - **Token Endpoint**: `https://oauth2.googleapis.com/token`
924
+ - **User Info Endpoint**: `https://www.googleapis.com/oauth2/v2/userinfo`
925
+ - **Supports**: PKCE, ID tokens, refresh tokens
926
+ - **Common Scopes**:
927
+ - `openid` - Required for OpenID Connect
928
+ - `email` - Access to user's email address
929
+ - `profile` - Access to user's basic profile information
930
+ - `https://www.googleapis.com/auth/drive.readonly` - Read-only access to Google Drive
931
+ - `https://www.googleapis.com/auth/calendar.readonly` - Read-only access to Google Calendar
932
+
933
+ [Full list of Google OAuth scopes](https://developers.google.com/identity/protocols/oauth2/scopes)
934
+
935
+ ### GitHub (`GithubProvider`)
936
+
937
+ ```typescript
938
+ import { GithubProvider } from "@vunexa/lixa-providers";
939
+ ```
940
+
941
+ - **Type**: OAuth 2.0
942
+ - **Authorization Endpoint**: `https://github.com/login/oauth/authorize`
943
+ - **Token Endpoint**: `https://github.com/login/oauth/access_token`
944
+ - **User Info Endpoint**: `https://api.github.com/user`
945
+ - **Supports**: PKCE
946
+ - **Common Scopes**:
947
+ - `user` - Read/write access to profile info
948
+ - `user:email` - Read access to user's email addresses
949
+ - `read:user` - Read-only access to profile info
950
+ - `repo` - Full control of private repositories
951
+ - `public_repo` - Access to public repositories
952
+ - `gist` - Create gists
953
+ - `read:org` - Read-only access to organization membership
954
+
955
+ [Full list of GitHub OAuth scopes](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps)
956
+
957
+ ## Extensibility Interfaces
958
+
959
+ ### SessionHandler Interface
960
+
961
+ The `SessionHandler` interface allows you to customize session generation and storage:
962
+
963
+ ```typescript
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>;
976
+ }
977
+
978
+ interface Session<TRaw = OAuthTokenResponse> {
979
+ token: string; // Your session identifier
980
+ raw: TRaw; // Session data (defaults to OAuthTokenResponse)
981
+ }
982
+
983
+ interface OAuthTokenResponse {
984
+ access_token: string; // Required
985
+ token_type: string; // Required
986
+ expires_in?: number; // Optional
987
+ refresh_token?: string; // Optional
988
+ scope?: string; // Optional
989
+ id_token?: string; // Optional (OIDC)
990
+ [key: string]: unknown; // Provider-specific fields
991
+ }
992
+
993
+ interface ProviderMetadata {
994
+ name: string;
995
+ endpoints: {
996
+ authorization: string;
997
+ token: string;
998
+ userInfo: string;
999
+ };
1000
+ }
1001
+ ```
1002
+
1003
+ **Use cases:**
1004
+ - Extract user info from OAuth tokens
1005
+ - Create or update users in your database
1006
+ - Store tokens securely in your database
1007
+ - Add custom claims or metadata
1008
+ - Implement custom session storage (Redis, database, etc.)
1009
+
1010
+ **Example with database integration:**
1011
+ ```typescript
1012
+ import { SessionHandler, OAuthTokenResponse, Session, extractUserInfo } from "@vunexa/lixa";
1013
+
1014
+ interface CustomSessionData extends OAuthTokenResponse {
1015
+ userId: string;
1016
+ }
1017
+
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);
1022
+
1023
+ // Create/update user
1024
+ const user = await db.users.upsert({
1025
+ email: userInfo.email,
1026
+ name: userInfo.name
1027
+ });
1028
+
1029
+ return {
1030
+ token: tokenData.access_token,
1031
+ raw: {
1032
+ ...tokenData,
1033
+ userId: user.id
1034
+ }
1035
+ };
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
+ },
1056
+ };
1057
+ ```
1058
+
1059
+ ### StateHandler Interface
1060
+
1061
+ The `StateHandler` interface manages OAuth state generation and storage for CSRF protection and PKCE:
1062
+
1063
+ ```typescript
1064
+ interface StateHandler {
1065
+ generateState?(provider: string): Promise<{ state: string; data: StateData }>;
1066
+ stateStorage?: StateStorage;
1067
+ }
1068
+
1069
+ interface StateStorage {
1070
+ saveState(state: string, data: StateData, expiresInSeconds: number): Promise<void>;
1071
+ getState(state: string): Promise<StateData | null>;
1072
+ deleteState(state: string): Promise<void>;
1073
+ }
1074
+
1075
+ interface StateData {
1076
+ provider: string; // Provider name for routing
1077
+ codeVerifier: string; // PKCE code verifier (64 hex chars)
1078
+ createdAt: number; // Unix timestamp in milliseconds
1079
+ }
1080
+ ```
1081
+
1082
+ **StateData fields:**
1083
+ - `provider`: Used to route callbacks to the correct provider configuration
1084
+ - `codeVerifier`: Required for PKCE token exchange (RFC 7636)
1085
+ - `createdAt`: Timestamp for debugging and validation
1086
+
1087
+ **Example with Redis:**
1088
+ ```typescript
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
+ }
1107
+ }
1108
+ };
1109
+ ```
1110
+
1111
+ ### SessionStorage Interface
1112
+
1113
+ The `SessionStorage` interface manages persistent user session storage:
1114
+
1115
+ ```typescript
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>;
1119
+ deleteSession(sessionId: string): Promise<void>;
1120
+ }
1121
+ ```
1122
+
1123
+ **Example with database:**
1124
+ ```typescript
1125
+ import { SessionStorage, Session } from "@vunexa/lixa";
1126
+
1127
+ const dbSessionStorage: SessionStorage = {
1128
+ saveSession: async <T extends Session>(sessionId: string, session: T, expiresInSeconds: number) => {
1129
+ const expiresAt = new Date(Date.now() + expiresInSeconds * 1000);
1130
+ await db.sessions.create({
1131
+ id: sessionId,
1132
+ data: JSON.stringify(session),
1133
+ expiresAt
1134
+ });
1135
+ },
1136
+
1137
+ getSession: async <T extends Session>(sessionId: string): Promise<T | null> => {
1138
+ const session = await db.sessions.findOne({
1139
+ id: sessionId,
1140
+ expiresAt: { $gt: new Date() }
1141
+ });
1142
+ return session ? JSON.parse(session.data) as T : null;
1143
+ },
1144
+
1145
+ deleteSession: async (sessionId: string) => {
1146
+ await db.sessions.delete({ id: sessionId });
1147
+ }
1148
+ };
1149
+ ```
1150
+
1151
+ ## PKCE Implementation
1152
+
1153
+ Lixa automatically implements **PKCE (Proof Key for Code Exchange)** according to [RFC 7636](https://tools.ietf.org/html/rfc7636) for all OAuth flows.
1154
+
1155
+ ### How it Works
1156
+
1157
+ 1. **Code Verifier Generation**
1158
+ - Generates 32 cryptographically random bytes
1159
+ - Encodes as 64-character hexadecimal string
1160
+ - Example: `a1b2c3d4e5f6...` (64 chars)
1161
+
1162
+ 2. **Code Challenge Generation**
1163
+ - Creates SHA-256 hash of the code verifier
1164
+ - Encodes as base64url (RFC 4648)
1165
+ - Sends with authorization request
1166
+
1167
+ 3. **Token Exchange**
1168
+ - Retrieves code verifier from state storage
1169
+ - Sends original code verifier with token request
1170
+ - Provider validates: `SHA256(code_verifier) === code_challenge`
1171
+
1172
+ ### Security Benefits
1173
+
1174
+ - **Prevents authorization code interception**: Even if an attacker intercepts the authorization code, they cannot exchange it for tokens without the code verifier
1175
+ - **No client secret required**: PKCE works without client secrets, making it suitable for public clients
1176
+ - **Standards compliant**: Follows RFC 7636 specification
1177
+
1178
+ ### Debug Logging
1179
+
1180
+ Enable debug mode to see PKCE flow details:
1181
+
1182
+ ```typescript
1183
+ const lixa = new Lixa({
1184
+ providers: { /* ... */ },
1185
+ debug: true
1186
+ });
1187
+ ```
1188
+
1189
+ Debug output includes:
1190
+ - `[Lixa] [timestamp] [INFO] [Auth]` - Code verifier and challenge generation
1191
+ - `[Lixa] [timestamp] [INFO] [State]` - State storage with code verifier
1192
+ - `[Lixa] [timestamp] [INFO] [Token]` - Token exchange with code verifier
1193
+
1194
+ ## Security Considerations
1195
+
1196
+ ### PKCE (Proof Key for Code Exchange)
1197
+
1198
+ Lixa automatically implements PKCE for all OAuth flows:
1199
+ - Generates a cryptographically secure code verifier
1200
+ - Creates SHA-256 code challenge
1201
+ - Stores code verifier with state
1202
+ - Sends code verifier during token exchange
1203
+
1204
+ This protects against authorization code interception attacks.
1205
+
1206
+ ### State Parameter
1207
+
1208
+ The state parameter prevents CSRF attacks:
1209
+ - Always generated using cryptographically secure random bytes
1210
+ - Must be validated on callback
1211
+ - Automatically stored and validated when using custom `stateDao`
1212
+ - Single-use (deleted after validation)
1213
+
1214
+ ### Best Practices
1215
+
1216
+ 1. **Always use HTTPS** in production for redirect URIs
1217
+ 2. **Implement custom storage** (StateDao/SessionDao) for production
1218
+ 3. **Set short TTLs** for state (5 minutes) and sessions (24 hours recommended)
1219
+ 4. **Validate state parameter** on every callback
1220
+ 5. **Store tokens securely** - never expose access/refresh tokens to client-side code
1221
+ 6. **Use HttpOnly cookies** for session tokens
1222
+ 7. **Enable debug mode** only in development
218
1223
 
219
1224
  ## Development
220
1225