@vunexa/lixa 0.0.1-alpha.19 → 0.0.1-alpha.20

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 (2) hide show
  1. package/README.md +293 -11
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -123,6 +123,158 @@ app.get("/auth/:provider/callback", async (req, res) => {
123
123
 
124
124
  ## Advanced Usage
125
125
 
126
+ ### Custom State Storage (StateDao)
127
+
128
+ By default, Lixa uses an in-memory cache for state management. For production applications or serverless environments, you should implement a custom state storage:
129
+
130
+ ```typescript
131
+ import { Lixa } from "@vunexa/lixa";
132
+
133
+ // Example: DynamoDB state storage
134
+ const dynamoDbStateDao = {
135
+ saveState: async (state: string, data: any, expiresInSeconds: number) => {
136
+ await dynamoDB.put({
137
+ TableName: "oauth-states",
138
+ Item: {
139
+ state,
140
+ data: JSON.stringify(data),
141
+ expiresAt: Math.floor(Date.now() / 1000) + expiresInSeconds,
142
+ },
143
+ });
144
+ },
145
+
146
+ getState: async (state: string) => {
147
+ const result = await dynamoDB.get({
148
+ TableName: "oauth-states",
149
+ Key: { state },
150
+ });
151
+
152
+ if (!result.Item) return null;
153
+
154
+ if (result.Item.expiresAt < Math.floor(Date.now() / 1000)) {
155
+ return null; // Expired
156
+ }
157
+
158
+ return JSON.parse(result.Item.data);
159
+ },
160
+
161
+ deleteState: async (state: string) => {
162
+ await dynamoDB.delete({
163
+ TableName: "oauth-states",
164
+ Key: { state },
165
+ });
166
+ },
167
+ };
168
+
169
+ const lixa = new Lixa({
170
+ providers: { /* ... */ },
171
+ stateDao: dynamoDbStateDao,
172
+ });
173
+ ```
174
+
175
+ **Important**: The state data includes a `codeVerifier` field required for PKCE. Make sure your storage preserves all fields in the data object.
176
+
177
+ ### Custom Session Storage (SessionDao)
178
+
179
+ Similar to state storage, you can implement custom session storage:
180
+
181
+ ```typescript
182
+ const customSessionDao = {
183
+ saveSession: async (sessionId: string, session: any, expiresInSeconds: number) => {
184
+ // Store session in your database
185
+ await db.sessions.create({
186
+ id: sessionId,
187
+ data: session,
188
+ expiresAt: Date.now() + expiresInSeconds * 1000,
189
+ });
190
+ },
191
+
192
+ getSession: async (sessionId: string) => {
193
+ const session = await db.sessions.findById(sessionId);
194
+ if (!session || session.expiresAt < Date.now()) {
195
+ return null;
196
+ }
197
+ return session.data;
198
+ },
199
+
200
+ deleteSession: async (sessionId: string) => {
201
+ await db.sessions.delete(sessionId);
202
+ },
203
+ };
204
+
205
+ const lixa = new Lixa({
206
+ providers: { /* ... */ },
207
+ sessionDao: customSessionDao,
208
+ });
209
+ ```
210
+
211
+ ### Custom Session Strategy
212
+
213
+ The session strategy controls how user sessions are created from OAuth tokens:
214
+
215
+ ```typescript
216
+ const customSessionStrategy = {
217
+ createSession: async (tokenData) => {
218
+ // tokenData contains: access_token, refresh_token, id_token, etc.
219
+
220
+ // Decode ID token to get user info (for OIDC providers like Google)
221
+ let userInfo;
222
+ if (tokenData.id_token) {
223
+ const base64Payload = tokenData.id_token.split('.')[1];
224
+ const payload = Buffer.from(base64Payload, 'base64').toString();
225
+ userInfo = JSON.parse(payload);
226
+ }
227
+
228
+ // Create or update user in your database
229
+ const user = await db.users.upsert({
230
+ email: userInfo.email,
231
+ name: userInfo.name,
232
+ // ... other fields
233
+ });
234
+
235
+ // Create session in your database
236
+ const sessionId = generateUniqueId();
237
+ await db.sessions.create({
238
+ id: sessionId,
239
+ userId: user.id,
240
+ accessToken: tokenData.access_token,
241
+ refreshToken: tokenData.refresh_token,
242
+ });
243
+
244
+ return {
245
+ token: sessionId,
246
+ raw: {
247
+ ...tokenData,
248
+ userId: user.id,
249
+ userInfo,
250
+ },
251
+ };
252
+ },
253
+ };
254
+
255
+ const lixa = new Lixa({
256
+ providers: { /* ... */ },
257
+ sessionStrategy: customSessionStrategy,
258
+ });
259
+ ```
260
+
261
+ ### Debug Mode
262
+
263
+ Enable debug logging to troubleshoot OAuth flows:
264
+
265
+ ```typescript
266
+ const lixa = new Lixa({
267
+ providers: { /* ... */ },
268
+ debug: true, // Enables detailed logging
269
+ });
270
+ ```
271
+
272
+ Debug mode logs:
273
+ - Token exchange requests and responses
274
+ - State validation
275
+ - Session creation
276
+ - Error details from OAuth providers
277
+
126
278
  ### Custom Provider Registration
127
279
 
128
280
  You can register custom OAuth providers by implementing the `IProvider` interface:
@@ -136,7 +288,7 @@ class CustomProvider implements IProvider {
136
288
  userInfoEndpoint = "https://custom-provider.com/api/user";
137
289
  }
138
290
 
139
- // Register the custom provider
291
+ // Register the custom provider BEFORE creating Lixa instance
140
292
  Lixa.registerProvider({
141
293
  custom: new CustomProvider(),
142
294
  });
@@ -161,6 +313,76 @@ const lixa = new Lixa({
161
313
  if (Lixa.isProviderRegistered("google")) {
162
314
  console.log("Google provider is available");
163
315
  }
316
+
317
+ // Get list of all registered providers
318
+ const providers = Lixa.getRegisteredProviders();
319
+ console.log("Available providers:", providers);
320
+ ```
321
+
322
+ ## Common Pitfalls & Solutions
323
+
324
+ ### 1. "Invalid or expired state" Error
325
+
326
+ **Cause**: The state parameter is not being stored or retrieved correctly.
327
+
328
+ **Solution**: Implement a custom `stateDao` that persists state across requests. The default in-memory cache doesn't work in serverless or multi-instance environments.
329
+
330
+ ### 2. "Missing code verifier" Error
331
+
332
+ **Cause**: The `codeVerifier` field is not being preserved in your state storage.
333
+
334
+ **Solution**: Ensure your `stateDao.saveState()` stores ALL fields from the data object, including `codeVerifier`:
335
+
336
+ ```typescript
337
+ saveState: async (state: string, data: any, expiresInSeconds: number) => {
338
+ // ✅ Correct: Store the entire data object
339
+ await storage.save({ state, data, expiresAt: ... });
340
+
341
+ // ❌ Wrong: Only storing some fields
342
+ await storage.save({ state, provider: data.provider, expiresAt: ... });
343
+ }
344
+ ```
345
+
346
+ ### 3. Token Exchange Fails with 400 Bad Request
347
+
348
+ **Causes**:
349
+ - Redirect URI mismatch between your config and OAuth provider console
350
+ - Invalid client ID or secret
351
+ - Code has already been used or expired
352
+
353
+ **Solution**:
354
+ - Enable debug mode to see the exact error from the provider
355
+ - Verify redirect URI matches exactly (including protocol, port, path)
356
+ - Check that client credentials are correct
357
+
358
+ ### 4. Session Not Found After Creation
359
+
360
+ **Cause**: Using default in-memory session storage in serverless/distributed environments.
361
+
362
+ **Solution**: Implement a custom `sessionDao` that uses persistent storage (database, Redis, etc.).
363
+
364
+ ### 5. User Info is Undefined
365
+
366
+ **Cause**: The `sessionStrategy.createSession()` receives raw token data, not user info.
367
+
368
+ **Solution**: Decode the `id_token` (for OIDC providers) or fetch user info from the provider's API:
369
+
370
+ ```typescript
371
+ createSession: async (tokenData) => {
372
+ // For OIDC providers (Google, etc.)
373
+ if (tokenData.id_token) {
374
+ const base64Payload = tokenData.id_token.split('.')[1];
375
+ const userInfo = JSON.parse(Buffer.from(base64Payload, 'base64').toString());
376
+ }
377
+
378
+ // For OAuth-only providers (GitHub, etc.)
379
+ else if (tokenData.access_token) {
380
+ const response = await fetch(providerUserInfoEndpoint, {
381
+ headers: { Authorization: `Bearer ${tokenData.access_token}` }
382
+ });
383
+ const userInfo = await response.json();
384
+ }
385
+ }
164
386
  ```
165
387
 
166
388
  ## API Reference
@@ -173,14 +395,16 @@ if (Lixa.isProviderRegistered("google")) {
173
395
 
174
396
  #### Static Methods
175
397
 
176
- - `Lixa.registerProvider(providerMap: { [key: string]: IProvider })` - Register custom providers
398
+ - `Lixa.registerProvider(providerMap: { [key: string]: IProvider })` - Register custom providers before creating instances
177
399
  - `Lixa.isProviderRegistered(provider: string): boolean` - Check if a provider is registered
400
+ - `Lixa.getRegisteredProviders(): string[]` - Get list of all registered provider names
401
+ - `Lixa.generateRandomState(): string` - Generate a cryptographically secure random state string
178
402
 
179
403
  #### Instance Methods
180
404
 
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
405
+ - `getAuthUrl(provider: string, state: string): string` - Generate authorization URL for a provider. Automatically stores state with PKCE code verifier.
406
+ - `handleCallback({ provider, code, state }): Promise<string>` - Handle OAuth callback, validate state, exchange code for tokens, and create session. Returns session ID.
407
+ - `fetchSessionInfo(sessionId: string): Promise<Session | null>` - Retrieve session information by session ID
184
408
 
185
409
  ### Types
186
410
 
@@ -190,21 +414,36 @@ interface ProviderConfig {
190
414
  clientSecret: string;
191
415
  redirectUri: string;
192
416
  scopes: string[];
193
- extraConfig?: Record<string, any>;
417
+ extraConfig?: Record<string, any>; // Provider-specific parameters
194
418
  }
195
419
 
196
420
  interface LixaConfig {
197
421
  providers: Record<string, ProviderConfig>;
198
422
  sessionStrategy?: SessionStrategy;
423
+ stateDao?: StateDao; // Custom state storage
424
+ sessionDao?: SessionDao; // Custom session storage
425
+ debug?: boolean; // Enable debug logging
199
426
  }
200
427
 
201
428
  interface SessionStrategy {
202
- createSession(userInfo: any): Promise<Session>;
429
+ createSession(tokenData: any): Promise<Session>;
203
430
  }
204
431
 
205
432
  interface Session {
206
- token: string;
207
- raw: any;
433
+ token: string; // Your custom session identifier
434
+ raw: any; // Raw data you want to store with the session
435
+ }
436
+
437
+ interface StateDao {
438
+ saveState(state: string, data: any, expiresInSeconds: number): Promise<void>;
439
+ getState(state: string): Promise<any | null>;
440
+ deleteState(state: string): Promise<void>;
441
+ }
442
+
443
+ interface SessionDao {
444
+ saveSession(sessionId: string, session: Session, expiresInSeconds: number): Promise<void>;
445
+ getSession(sessionId: string): Promise<Session | null>;
446
+ deleteSession(sessionId: string): Promise<void>;
208
447
  }
209
448
 
210
449
  interface IProvider {
@@ -216,8 +455,51 @@ interface IProvider {
216
455
 
217
456
  ## Built-in Providers
218
457
 
219
- - **Google** - OAuth 2.0 and OpenID Connect
220
- - **GitHub** - OAuth 2.0
458
+ ### Google
459
+ - **Type**: OAuth 2.0 + OpenID Connect (OIDC)
460
+ - **Authorization Endpoint**: `https://accounts.google.com/o/oauth2/v2/auth`
461
+ - **Token Endpoint**: `https://oauth2.googleapis.com/token`
462
+ - **User Info Endpoint**: `https://www.googleapis.com/oauth2/v2/userinfo`
463
+ - **Supports**: PKCE, ID tokens, refresh tokens
464
+ - **Common Scopes**: `openid`, `email`, `profile`
465
+
466
+ ### GitHub
467
+ - **Type**: OAuth 2.0
468
+ - **Authorization Endpoint**: `https://github.com/login/oauth/authorize`
469
+ - **Token Endpoint**: `https://github.com/login/oauth/access_token`
470
+ - **User Info Endpoint**: `https://api.github.com/user`
471
+ - **Supports**: PKCE
472
+ - **Common Scopes**: `read:user`, `user:email`
473
+
474
+ ## Security Considerations
475
+
476
+ ### PKCE (Proof Key for Code Exchange)
477
+
478
+ Lixa automatically implements PKCE for all OAuth flows:
479
+ - Generates a cryptographically secure code verifier
480
+ - Creates SHA-256 code challenge
481
+ - Stores code verifier with state
482
+ - Sends code verifier during token exchange
483
+
484
+ This protects against authorization code interception attacks.
485
+
486
+ ### State Parameter
487
+
488
+ The state parameter prevents CSRF attacks:
489
+ - Always generated using cryptographically secure random bytes
490
+ - Must be validated on callback
491
+ - Automatically stored and validated when using custom `stateDao`
492
+ - Single-use (deleted after validation)
493
+
494
+ ### Best Practices
495
+
496
+ 1. **Always use HTTPS** in production for redirect URIs
497
+ 2. **Implement custom storage** (StateDao/SessionDao) for production
498
+ 3. **Set short TTLs** for state (5 minutes) and sessions (24 hours recommended)
499
+ 4. **Validate state parameter** on every callback
500
+ 5. **Store tokens securely** - never expose access/refresh tokens to client-side code
501
+ 6. **Use HttpOnly cookies** for session tokens
502
+ 7. **Enable debug mode** only in development
221
503
 
222
504
  ## Development
223
505
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vunexa/lixa",
3
- "version": "0.0.1-alpha.19",
3
+ "version": "0.0.1-alpha.20",
4
4
  "description": "Lixa is a flexible, provider-agnostic OAuth and OpenID Connect (OIDC) client library that simplifies multi-provider authentication flows. It supports seamless integration with providers like Google and GitHub, offers extensible session management, and enables dynamic provider resolution based on callback URLs.",
5
5
  "keywords": [
6
6
  "oauth",