@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 CHANGED
@@ -33,14 +33,22 @@ yarn add @vunexa/lixa
33
33
 
34
34
  ## Quick Start
35
35
 
36
- ### 1. Configure lixa with multiple providers
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
- ### 2. Redirect users to the provider's authorization URL
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 = lixa.generateRandomState();
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.toUpperCase(), state);
93
+ const authUrl = lixa.getAuthUrl(provider, state);
86
94
  res.redirect(authUrl);
87
95
  });
88
96
  ```
89
97
 
90
- ### 3. Handle the provider callback and establish a session
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 session = await lixa.handleCallback({
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
- res.cookie("session_token", session.token, {
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 Provider Registration
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
- You can register custom OAuth providers by implementing the `IProvider` interface:
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 Registration
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
- // Check if a provider is registered
161
- if (Lixa.isProviderRegistered("google")) {
162
- console.log("Google provider is available");
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.registerProvider(providerMap: { [key: string]: IProvider })` - Register custom providers
177
- - `Lixa.isProviderRegistered(provider: string): boolean` - Check if a provider is registered
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
- - `generateRandomState(): string` - Generate a random state parameter for OAuth flow
182
- - `getAuthUrl(provider: string, state: string): string` - Get authorization URL for a provider
183
- - `handleCallback({ provider, code, state }): Promise<Session>` - Handle OAuth callback and create session
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
- interface ProviderConfig {
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: Record<string, ProviderConfig>;
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(userInfo: any): Promise<Session>;
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
- - **Google** - OAuth 2.0 and OpenID Connect
220
- - **GitHub** - OAuth 2.0
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