@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.
- package/README.md +293 -11
- 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
|
-
- `
|
|
182
|
-
- `
|
|
183
|
-
- `
|
|
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(
|
|
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
|
-
|
|
220
|
-
- **
|
|
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.
|
|
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",
|