@vunexa/lixa 0.0.1-alpha.20 → 0.0.1-alpha.22
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 +562 -79
- package/dist/dao/session-cache.d.ts +2 -2
- package/dist/dao/session-cache.d.ts.map +1 -1
- package/dist/dao/state-cache.d.ts +3 -3
- package/dist/dao/state-cache.d.ts.map +1 -1
- package/dist/dao/types.d.ts +246 -5
- package/dist/dao/types.d.ts.map +1 -1
- package/dist/export-types/index.d.ts +852 -49
- package/dist/index.cjs +282 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +853 -46
- package/dist/index.d.ts +11 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +282 -46
- package/dist/index.js.map +1 -1
- package/dist/lixa.d.ts +187 -18
- package/dist/lixa.d.ts.map +1 -1
- package/dist/models/session.d.ts +163 -7
- package/dist/models/session.d.ts.map +1 -1
- package/dist/providers/IProvider.d.ts +121 -4
- package/dist/providers/IProvider.d.ts.map +1 -1
- package/dist/providers/index.d.ts +0 -2
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/types.d.ts +131 -15
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -16
- package/dist/IProvider-2tDwRAnH.d.cts +0 -18
- package/dist/IProvider-2tDwRAnH.d.ts +0 -18
- package/dist/export-types/providers.d.ts +0 -61
- package/dist/providers/github.d.ts +0 -13
- package/dist/providers/github.d.ts.map +0 -1
- package/dist/providers/google.d.ts +0 -13
- package/dist/providers/google.d.ts.map +0 -1
- package/dist/providers-entry.cjs +0 -48
- package/dist/providers-entry.cjs.map +0 -1
- package/dist/providers-entry.d.cts +0 -25
- package/dist/providers-entry.d.ts +0 -22
- package/dist/providers-entry.d.ts.map +0 -1
- package/dist/providers-entry.js +0 -20
- package/dist/providers-entry.js.map +0 -1
package/README.md
CHANGED
|
@@ -18,7 +18,8 @@ A flexible, provider-agnostic OAuth 2.0 and OpenID Connect (OIDC) client library
|
|
|
18
18
|
- Custom provider registration with extensible provider interface
|
|
19
19
|
- Extensible session management via pluggable strategies
|
|
20
20
|
- PKCE (Proof Key for Code Exchange) support
|
|
21
|
-
- TypeScript-first with
|
|
21
|
+
- TypeScript-first with full type safety (zero `any` types)
|
|
22
|
+
- OAuth 2.0 and OpenID Connect spec-compliant types
|
|
22
23
|
- 100% test coverage with comprehensive error handling
|
|
23
24
|
|
|
24
25
|
---
|
|
@@ -33,14 +34,22 @@ yarn add @vunexa/lixa
|
|
|
33
34
|
|
|
34
35
|
## Quick Start
|
|
35
36
|
|
|
36
|
-
### 1.
|
|
37
|
+
### 1. Install packages
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install @vunexa/lixa @vunexa/lixa-providers
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 2. Configure Lixa with providers
|
|
37
44
|
|
|
38
45
|
```typescript
|
|
39
46
|
import { Lixa } from "@vunexa/lixa";
|
|
47
|
+
import { GoogleProvider, GithubProvider } from "@vunexa/lixa-providers";
|
|
40
48
|
|
|
41
49
|
const lixa = new Lixa({
|
|
42
50
|
providers: {
|
|
43
51
|
google: {
|
|
52
|
+
provider: new GoogleProvider(),
|
|
44
53
|
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
45
54
|
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
46
55
|
redirectUri: "https://yourapp.com/auth/google/callback",
|
|
@@ -51,20 +60,21 @@ const lixa = new Lixa({
|
|
|
51
60
|
},
|
|
52
61
|
},
|
|
53
62
|
github: {
|
|
63
|
+
provider: new GithubProvider(),
|
|
54
64
|
clientId: process.env.GITHUB_CLIENT_ID!,
|
|
55
65
|
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
56
66
|
redirectUri: "https://yourapp.com/auth/github/callback",
|
|
57
67
|
scopes: ["read:user", "user:email"],
|
|
58
|
-
extraConfig: {},
|
|
59
68
|
},
|
|
60
69
|
},
|
|
61
70
|
|
|
62
71
|
// Optional: custom session strategy
|
|
63
72
|
sessionStrategy: {
|
|
64
73
|
createSession: async (tokenData) => {
|
|
65
|
-
//
|
|
74
|
+
// tokenData is typed as OAuthTokenResponse with proper OAuth 2.0 fields
|
|
75
|
+
// { access_token, token_type, expires_in?, refresh_token?, scope?, id_token? }
|
|
66
76
|
return {
|
|
67
|
-
token:
|
|
77
|
+
token: tokenData.access_token,
|
|
68
78
|
raw: tokenData,
|
|
69
79
|
};
|
|
70
80
|
},
|
|
@@ -72,22 +82,22 @@ const lixa = new Lixa({
|
|
|
72
82
|
});
|
|
73
83
|
```
|
|
74
84
|
|
|
75
|
-
###
|
|
85
|
+
### 3. Redirect users to the provider's authorization URL
|
|
76
86
|
|
|
77
87
|
```typescript
|
|
78
88
|
app.get("/login", (req, res) => {
|
|
79
89
|
const provider = req.query.provider as string; // 'google' or 'github'
|
|
80
|
-
const state =
|
|
90
|
+
const state = Lixa.generateRandomState();
|
|
81
91
|
|
|
82
92
|
// Store state in session for validation
|
|
83
93
|
req.session.oauthState = state;
|
|
84
94
|
|
|
85
|
-
const authUrl = lixa.getAuthUrl(provider
|
|
95
|
+
const authUrl = lixa.getAuthUrl(provider, state);
|
|
86
96
|
res.redirect(authUrl);
|
|
87
97
|
});
|
|
88
98
|
```
|
|
89
99
|
|
|
90
|
-
###
|
|
100
|
+
### 4. Handle the provider callback and establish a session
|
|
91
101
|
|
|
92
102
|
```typescript
|
|
93
103
|
app.get("/auth/:provider/callback", async (req, res) => {
|
|
@@ -100,14 +110,17 @@ app.get("/auth/:provider/callback", async (req, res) => {
|
|
|
100
110
|
throw new Error("Invalid state parameter");
|
|
101
111
|
}
|
|
102
112
|
|
|
103
|
-
const
|
|
113
|
+
const sessionId = await lixa.handleCallback({
|
|
104
114
|
provider,
|
|
105
115
|
code: code as string,
|
|
106
116
|
state: state as string,
|
|
107
117
|
});
|
|
108
118
|
|
|
109
|
-
// Session established
|
|
110
|
-
|
|
119
|
+
// Session established - retrieve session info if needed
|
|
120
|
+
const session = await lixa.fetchSessionInfo(sessionId);
|
|
121
|
+
|
|
122
|
+
// Store session ID in cookie
|
|
123
|
+
res.cookie("session_id", sessionId, {
|
|
111
124
|
httpOnly: true,
|
|
112
125
|
secure: true,
|
|
113
126
|
sameSite: "strict",
|
|
@@ -121,6 +134,118 @@ app.get("/auth/:provider/callback", async (req, res) => {
|
|
|
121
134
|
});
|
|
122
135
|
```
|
|
123
136
|
|
|
137
|
+
## Provider Configuration
|
|
138
|
+
|
|
139
|
+
### Using Built-in Providers
|
|
140
|
+
|
|
141
|
+
Built-in providers are available in the `@vunexa/lixa-providers` package:
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import { Lixa } from "@vunexa/lixa";
|
|
145
|
+
import { GoogleProvider, GithubProvider } from "@vunexa/lixa-providers";
|
|
146
|
+
|
|
147
|
+
const lixa = new Lixa({
|
|
148
|
+
providers: {
|
|
149
|
+
google: {
|
|
150
|
+
provider: new GoogleProvider(),
|
|
151
|
+
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
152
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
153
|
+
redirectUri: "https://yourapp.com/auth/google/callback",
|
|
154
|
+
scopes: ["openid", "email", "profile"]
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Using Custom Inline Providers
|
|
161
|
+
|
|
162
|
+
You can create custom providers by implementing the `IProvider` interface:
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
import { Lixa, IProvider } from "@vunexa/lixa";
|
|
166
|
+
|
|
167
|
+
// Define your custom provider
|
|
168
|
+
const customProvider: IProvider = {
|
|
169
|
+
authorizationEndpoint: "https://provider.com/oauth/authorize",
|
|
170
|
+
tokenEndpoint: "https://provider.com/oauth/token",
|
|
171
|
+
userInfoEndpoint: "https://provider.com/api/user"
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Use it directly in configuration
|
|
175
|
+
const lixa = new Lixa({
|
|
176
|
+
providers: {
|
|
177
|
+
custom: {
|
|
178
|
+
provider: customProvider,
|
|
179
|
+
clientId: "your-client-id",
|
|
180
|
+
clientSecret: "your-client-secret",
|
|
181
|
+
redirectUri: "https://yourapp.com/auth/custom/callback",
|
|
182
|
+
scopes: ["read:user"]
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Mixing Built-in and Custom Providers
|
|
189
|
+
|
|
190
|
+
You can use both built-in and custom providers in the same configuration:
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
import { Lixa, IProvider } from "@vunexa/lixa";
|
|
194
|
+
import { GoogleProvider } from "@vunexa/lixa-providers";
|
|
195
|
+
|
|
196
|
+
const customProvider: IProvider = {
|
|
197
|
+
authorizationEndpoint: "https://custom.com/oauth/authorize",
|
|
198
|
+
tokenEndpoint: "https://custom.com/oauth/token",
|
|
199
|
+
userInfoEndpoint: "https://custom.com/api/user"
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const lixa = new Lixa({
|
|
203
|
+
providers: {
|
|
204
|
+
google: {
|
|
205
|
+
provider: new GoogleProvider(),
|
|
206
|
+
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
207
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
208
|
+
redirectUri: "https://yourapp.com/auth/google/callback",
|
|
209
|
+
scopes: ["openid", "email", "profile"]
|
|
210
|
+
},
|
|
211
|
+
custom: {
|
|
212
|
+
provider: customProvider,
|
|
213
|
+
clientId: process.env.CUSTOM_CLIENT_ID!,
|
|
214
|
+
clientSecret: process.env.CUSTOM_CLIENT_SECRET!,
|
|
215
|
+
redirectUri: "https://yourapp.com/auth/custom/callback",
|
|
216
|
+
scopes: ["read:user"]
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Overriding Built-in Providers (Testing)
|
|
223
|
+
|
|
224
|
+
You can override built-in providers with custom implementations for testing:
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
import { Lixa, IProvider } from "@vunexa/lixa";
|
|
228
|
+
|
|
229
|
+
// Mock provider for testing
|
|
230
|
+
const mockGoogleProvider: IProvider = {
|
|
231
|
+
authorizationEndpoint: "http://localhost:3000/mock/authorize",
|
|
232
|
+
tokenEndpoint: "http://localhost:3000/mock/token",
|
|
233
|
+
userInfoEndpoint: "http://localhost:3000/mock/userinfo"
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const lixa = new Lixa({
|
|
237
|
+
providers: {
|
|
238
|
+
google: {
|
|
239
|
+
provider: mockGoogleProvider, // Override with mock
|
|
240
|
+
clientId: "test-client-id",
|
|
241
|
+
clientSecret: "test-client-secret",
|
|
242
|
+
redirectUri: "http://localhost:3000/callback",
|
|
243
|
+
scopes: ["openid", "email"]
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
```
|
|
248
|
+
|
|
124
249
|
## Advanced Usage
|
|
125
250
|
|
|
126
251
|
### Custom State Storage (StateDao)
|
|
@@ -128,11 +253,11 @@ app.get("/auth/:provider/callback", async (req, res) => {
|
|
|
128
253
|
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
254
|
|
|
130
255
|
```typescript
|
|
131
|
-
import { Lixa } from "@vunexa/lixa";
|
|
256
|
+
import { Lixa, StateDao, StateData } from "@vunexa/lixa";
|
|
132
257
|
|
|
133
|
-
// Example: DynamoDB state storage
|
|
134
|
-
const dynamoDbStateDao = {
|
|
135
|
-
saveState: async (state: string, data:
|
|
258
|
+
// Example: DynamoDB state storage with proper typing
|
|
259
|
+
const dynamoDbStateDao: StateDao = {
|
|
260
|
+
saveState: async (state: string, data: StateData, expiresInSeconds: number) => {
|
|
136
261
|
await dynamoDB.put({
|
|
137
262
|
TableName: "oauth-states",
|
|
138
263
|
Item: {
|
|
@@ -143,7 +268,7 @@ const dynamoDbStateDao = {
|
|
|
143
268
|
});
|
|
144
269
|
},
|
|
145
270
|
|
|
146
|
-
getState: async (state: string) => {
|
|
271
|
+
getState: async (state: string): Promise<StateData | null> => {
|
|
147
272
|
const result = await dynamoDB.get({
|
|
148
273
|
TableName: "oauth-states",
|
|
149
274
|
Key: { state },
|
|
@@ -155,7 +280,7 @@ const dynamoDbStateDao = {
|
|
|
155
280
|
return null; // Expired
|
|
156
281
|
}
|
|
157
282
|
|
|
158
|
-
return JSON.parse(result.Item.data);
|
|
283
|
+
return JSON.parse(result.Item.data) as StateData;
|
|
159
284
|
},
|
|
160
285
|
|
|
161
286
|
deleteState: async (state: string) => {
|
|
@@ -172,15 +297,22 @@ const lixa = new Lixa({
|
|
|
172
297
|
});
|
|
173
298
|
```
|
|
174
299
|
|
|
175
|
-
**Important**: The
|
|
300
|
+
**Important**: The `StateData` type includes required fields:
|
|
301
|
+
- `provider` (string): Provider name for callback routing
|
|
302
|
+
- `codeVerifier` (string): PKCE code verifier (64 hex characters)
|
|
303
|
+
- `createdAt` (number): Unix timestamp in milliseconds
|
|
304
|
+
|
|
305
|
+
Make sure your storage preserves all fields in the data object.
|
|
176
306
|
|
|
177
307
|
### Custom Session Storage (SessionDao)
|
|
178
308
|
|
|
179
|
-
Similar to state storage, you can implement custom session storage:
|
|
309
|
+
Similar to state storage, you can implement custom session storage with proper typing:
|
|
180
310
|
|
|
181
311
|
```typescript
|
|
182
|
-
|
|
183
|
-
|
|
312
|
+
import { Lixa, SessionDao, Session } from "@vunexa/lixa";
|
|
313
|
+
|
|
314
|
+
const customSessionDao: SessionDao = {
|
|
315
|
+
saveSession: async <T = unknown>(sessionId: string, session: T, expiresInSeconds: number) => {
|
|
184
316
|
// Store session in your database
|
|
185
317
|
await db.sessions.create({
|
|
186
318
|
id: sessionId,
|
|
@@ -189,12 +321,12 @@ const customSessionDao = {
|
|
|
189
321
|
});
|
|
190
322
|
},
|
|
191
323
|
|
|
192
|
-
getSession: async (sessionId: string) => {
|
|
324
|
+
getSession: async <T = unknown>(sessionId: string): Promise<T | null> => {
|
|
193
325
|
const session = await db.sessions.findById(sessionId);
|
|
194
326
|
if (!session || session.expiresAt < Date.now()) {
|
|
195
327
|
return null;
|
|
196
328
|
}
|
|
197
|
-
return session.data;
|
|
329
|
+
return session.data as T;
|
|
198
330
|
},
|
|
199
331
|
|
|
200
332
|
deleteSession: async (sessionId: string) => {
|
|
@@ -210,12 +342,29 @@ const lixa = new Lixa({
|
|
|
210
342
|
|
|
211
343
|
### Custom Session Strategy
|
|
212
344
|
|
|
213
|
-
The session strategy controls how user sessions are created from OAuth tokens:
|
|
345
|
+
The session strategy controls how user sessions are created from OAuth tokens. The `tokenData` parameter is typed as `OAuthTokenResponse` with proper OAuth 2.0 fields:
|
|
214
346
|
|
|
215
347
|
```typescript
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
348
|
+
import { Lixa, SessionStrategy, OAuthTokenResponse, Session } from "@vunexa/lixa";
|
|
349
|
+
|
|
350
|
+
// Define custom session data structure
|
|
351
|
+
interface CustomSessionData extends OAuthTokenResponse {
|
|
352
|
+
userId: string;
|
|
353
|
+
userInfo: {
|
|
354
|
+
email: string;
|
|
355
|
+
name: string;
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const customSessionStrategy: SessionStrategy = {
|
|
360
|
+
createSession: async (tokenData: OAuthTokenResponse): Promise<Session<CustomSessionData>> => {
|
|
361
|
+
// tokenData is properly typed with OAuth 2.0 fields:
|
|
362
|
+
// - access_token: string (required)
|
|
363
|
+
// - token_type: string (required)
|
|
364
|
+
// - expires_in?: number
|
|
365
|
+
// - refresh_token?: string
|
|
366
|
+
// - scope?: string
|
|
367
|
+
// - id_token?: string (for OIDC providers)
|
|
219
368
|
|
|
220
369
|
// Decode ID token to get user info (for OIDC providers like Google)
|
|
221
370
|
let userInfo;
|
|
@@ -275,9 +424,11 @@ Debug mode logs:
|
|
|
275
424
|
- Session creation
|
|
276
425
|
- Error details from OAuth providers
|
|
277
426
|
|
|
278
|
-
###
|
|
427
|
+
### Legacy Provider Registration (Deprecated)
|
|
279
428
|
|
|
280
|
-
|
|
429
|
+
> **Note**: This pattern is deprecated. Use inline providers instead (see examples above).
|
|
430
|
+
|
|
431
|
+
For backward compatibility, you can still register providers globally:
|
|
281
432
|
|
|
282
433
|
```typescript
|
|
283
434
|
import { Lixa, IProvider } from "@vunexa/lixa";
|
|
@@ -293,7 +444,7 @@ Lixa.registerProvider({
|
|
|
293
444
|
custom: new CustomProvider(),
|
|
294
445
|
});
|
|
295
446
|
|
|
296
|
-
// Use it in your configuration
|
|
447
|
+
// Use it in your configuration (without provider field)
|
|
297
448
|
const lixa = new Lixa({
|
|
298
449
|
providers: {
|
|
299
450
|
custom: {
|
|
@@ -306,17 +457,17 @@ const lixa = new Lixa({
|
|
|
306
457
|
});
|
|
307
458
|
```
|
|
308
459
|
|
|
309
|
-
### Check Provider
|
|
460
|
+
### Check Provider Configuration
|
|
310
461
|
|
|
311
462
|
```typescript
|
|
312
|
-
// Check if a provider is
|
|
313
|
-
if (
|
|
314
|
-
console.log("Google provider is
|
|
463
|
+
// Check if a provider is configured for this instance
|
|
464
|
+
if (lixa.isProviderConfigured("google")) {
|
|
465
|
+
console.log("Google provider is configured");
|
|
315
466
|
}
|
|
316
467
|
|
|
317
|
-
// Get list of all registered providers
|
|
468
|
+
// Get list of all registered providers (legacy)
|
|
318
469
|
const providers = Lixa.getRegisteredProviders();
|
|
319
|
-
console.log("
|
|
470
|
+
console.log("Registered providers:", providers);
|
|
320
471
|
```
|
|
321
472
|
|
|
322
473
|
## Common Pitfalls & Solutions
|
|
@@ -368,21 +519,30 @@ saveState: async (state: string, data: any, expiresInSeconds: number) => {
|
|
|
368
519
|
**Solution**: Decode the `id_token` (for OIDC providers) or fetch user info from the provider's API:
|
|
369
520
|
|
|
370
521
|
```typescript
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
522
|
+
import { SessionStrategy, OAuthTokenResponse } from "@vunexa/lixa";
|
|
523
|
+
|
|
524
|
+
const strategy: SessionStrategy = {
|
|
525
|
+
createSession: async (tokenData: OAuthTokenResponse) => {
|
|
526
|
+
// For OIDC providers (Google, etc.) - id_token is properly typed
|
|
527
|
+
if (tokenData.id_token) {
|
|
528
|
+
const base64Payload = tokenData.id_token.split('.')[1];
|
|
529
|
+
const userInfo = JSON.parse(Buffer.from(base64Payload, 'base64').toString());
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// For OAuth-only providers (GitHub, etc.) - access_token is guaranteed to exist
|
|
533
|
+
else {
|
|
534
|
+
const response = await fetch(providerUserInfoEndpoint, {
|
|
535
|
+
headers: { Authorization: `Bearer ${tokenData.access_token}` }
|
|
536
|
+
});
|
|
537
|
+
const userInfo = await response.json();
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return {
|
|
541
|
+
token: tokenData.access_token,
|
|
542
|
+
raw: tokenData
|
|
543
|
+
};
|
|
384
544
|
}
|
|
385
|
-
}
|
|
545
|
+
};
|
|
386
546
|
```
|
|
387
547
|
|
|
388
548
|
## API Reference
|
|
@@ -395,54 +555,77 @@ createSession: async (tokenData) => {
|
|
|
395
555
|
|
|
396
556
|
#### Static Methods
|
|
397
557
|
|
|
398
|
-
- `Lixa.
|
|
399
|
-
- `Lixa.
|
|
400
|
-
- `Lixa.getRegisteredProviders(): string[]` - Get list of all registered provider names
|
|
401
|
-
- `Lixa.generateRandomState(): string` - Generate a cryptographically secure random state string
|
|
558
|
+
- `Lixa.generateRandomState(): string` - Generate a cryptographically secure random state string for CSRF protection
|
|
559
|
+
- `Lixa.registerProvider(providerMap: { [key: string]: IProvider })` - **[Deprecated]** Register custom providers globally. Use inline providers instead.
|
|
560
|
+
- `Lixa.getRegisteredProviders(): string[]` - **[Deprecated]** Get list of all registered provider names from legacy registry
|
|
402
561
|
|
|
403
562
|
#### Instance Methods
|
|
404
563
|
|
|
405
|
-
- `getAuthUrl(provider: string, state: string): string` - Generate authorization URL for a provider. Automatically stores state with
|
|
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
|
|
564
|
+
- `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.
|
|
565
|
+
- `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.
|
|
566
|
+
- `fetchSessionInfo(sessionId: string): Promise<Session | null>` - Retrieve session information by session ID from SessionDao.
|
|
567
|
+
- `isProviderConfigured(provider: string): boolean` - Check if a provider is configured for this Lixa instance. Useful for type guards and runtime validation.
|
|
408
568
|
|
|
409
569
|
### Types
|
|
410
570
|
|
|
571
|
+
All types are fully typed with zero `any` types, following OAuth 2.0 and OpenID Connect specifications:
|
|
572
|
+
|
|
411
573
|
```typescript
|
|
412
|
-
|
|
574
|
+
// OAuth 2.0 token response (RFC 6749 Section 5.1)
|
|
575
|
+
interface OAuthTokenResponse {
|
|
576
|
+
access_token: string; // Required: OAuth access token
|
|
577
|
+
token_type: string; // Required: Token type (usually "Bearer")
|
|
578
|
+
expires_in?: number; // Optional: Token expiration in seconds
|
|
579
|
+
refresh_token?: string; // Optional: Refresh token
|
|
580
|
+
scope?: string; // Optional: Granted scopes (space-separated)
|
|
581
|
+
id_token?: string; // Optional: OpenID Connect ID token (JWT)
|
|
582
|
+
[key: string]: unknown; // Additional provider-specific fields
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Provider configuration with inline provider support
|
|
586
|
+
type ProviderConfig = {
|
|
413
587
|
clientId: string;
|
|
414
588
|
clientSecret: string;
|
|
415
589
|
redirectUri: string;
|
|
416
590
|
scopes: string[];
|
|
417
|
-
extraConfig?: Record<string,
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
591
|
+
extraConfig?: Record<string, string>;
|
|
592
|
+
} & (
|
|
593
|
+
| { provider?: never } // Built-in provider (no provider field)
|
|
594
|
+
| { provider: IProvider } // Custom provider (provider field required)
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
interface LixaConfig<TProviders = Record<string, ProviderConfig>> {
|
|
598
|
+
providers: TProviders;
|
|
422
599
|
sessionStrategy?: SessionStrategy;
|
|
423
|
-
stateDao?: StateDao;
|
|
424
|
-
sessionDao?: SessionDao;
|
|
425
|
-
debug?: boolean;
|
|
600
|
+
stateDao?: StateDao;
|
|
601
|
+
sessionDao?: SessionDao;
|
|
602
|
+
debug?: boolean;
|
|
426
603
|
}
|
|
427
604
|
|
|
428
605
|
interface SessionStrategy {
|
|
429
|
-
createSession(tokenData:
|
|
606
|
+
createSession(tokenData: OAuthTokenResponse): Promise<Session>;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
interface Session<TRaw = OAuthTokenResponse> {
|
|
610
|
+
token: string;
|
|
611
|
+
raw: TRaw; // Defaults to OAuthTokenResponse, can be extended
|
|
430
612
|
}
|
|
431
613
|
|
|
432
|
-
interface
|
|
433
|
-
|
|
434
|
-
|
|
614
|
+
interface StateData {
|
|
615
|
+
provider: string; // Provider name for routing
|
|
616
|
+
codeVerifier: string; // PKCE code verifier (64 hex chars)
|
|
617
|
+
createdAt: number; // Unix timestamp in milliseconds
|
|
435
618
|
}
|
|
436
619
|
|
|
437
620
|
interface StateDao {
|
|
438
|
-
saveState(state: string, data:
|
|
439
|
-
getState(state: string): Promise<
|
|
621
|
+
saveState(state: string, data: StateData, expiresInSeconds: number): Promise<void>;
|
|
622
|
+
getState(state: string): Promise<StateData | null>;
|
|
440
623
|
deleteState(state: string): Promise<void>;
|
|
441
624
|
}
|
|
442
625
|
|
|
443
626
|
interface SessionDao {
|
|
444
|
-
saveSession(sessionId: string, session:
|
|
445
|
-
getSession(sessionId: string): Promise<
|
|
627
|
+
saveSession<T = unknown>(sessionId: string, session: T, expiresInSeconds: number): Promise<void>;
|
|
628
|
+
getSession<T = unknown>(sessionId: string): Promise<T | null>;
|
|
446
629
|
deleteSession(sessionId: string): Promise<void>;
|
|
447
630
|
}
|
|
448
631
|
|
|
@@ -455,21 +638,321 @@ interface IProvider {
|
|
|
455
638
|
|
|
456
639
|
## Built-in Providers
|
|
457
640
|
|
|
458
|
-
|
|
641
|
+
Built-in providers are available in the `@vunexa/lixa-providers` package:
|
|
642
|
+
|
|
643
|
+
```bash
|
|
644
|
+
npm install @vunexa/lixa-providers
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
### Google (`GoogleProvider`)
|
|
648
|
+
|
|
649
|
+
```typescript
|
|
650
|
+
import { GoogleProvider } from "@vunexa/lixa-providers";
|
|
651
|
+
```
|
|
652
|
+
|
|
459
653
|
- **Type**: OAuth 2.0 + OpenID Connect (OIDC)
|
|
460
654
|
- **Authorization Endpoint**: `https://accounts.google.com/o/oauth2/v2/auth`
|
|
461
655
|
- **Token Endpoint**: `https://oauth2.googleapis.com/token`
|
|
462
656
|
- **User Info Endpoint**: `https://www.googleapis.com/oauth2/v2/userinfo`
|
|
463
657
|
- **Supports**: PKCE, ID tokens, refresh tokens
|
|
464
|
-
- **Common Scopes**:
|
|
658
|
+
- **Common Scopes**:
|
|
659
|
+
- `openid` - Required for OpenID Connect
|
|
660
|
+
- `email` - Access to user's email address
|
|
661
|
+
- `profile` - Access to user's basic profile information
|
|
662
|
+
- `https://www.googleapis.com/auth/drive.readonly` - Read-only access to Google Drive
|
|
663
|
+
- `https://www.googleapis.com/auth/calendar.readonly` - Read-only access to Google Calendar
|
|
664
|
+
|
|
665
|
+
[Full list of Google OAuth scopes](https://developers.google.com/identity/protocols/oauth2/scopes)
|
|
666
|
+
|
|
667
|
+
### GitHub (`GithubProvider`)
|
|
668
|
+
|
|
669
|
+
```typescript
|
|
670
|
+
import { GithubProvider } from "@vunexa/lixa-providers";
|
|
671
|
+
```
|
|
465
672
|
|
|
466
|
-
### GitHub
|
|
467
673
|
- **Type**: OAuth 2.0
|
|
468
674
|
- **Authorization Endpoint**: `https://github.com/login/oauth/authorize`
|
|
469
675
|
- **Token Endpoint**: `https://github.com/login/oauth/access_token`
|
|
470
676
|
- **User Info Endpoint**: `https://api.github.com/user`
|
|
471
677
|
- **Supports**: PKCE
|
|
472
|
-
- **Common Scopes**:
|
|
678
|
+
- **Common Scopes**:
|
|
679
|
+
- `user` - Read/write access to profile info
|
|
680
|
+
- `user:email` - Read access to user's email addresses
|
|
681
|
+
- `read:user` - Read-only access to profile info
|
|
682
|
+
- `repo` - Full control of private repositories
|
|
683
|
+
- `public_repo` - Access to public repositories
|
|
684
|
+
- `gist` - Create gists
|
|
685
|
+
- `read:org` - Read-only access to organization membership
|
|
686
|
+
|
|
687
|
+
[Full list of GitHub OAuth scopes](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps)
|
|
688
|
+
|
|
689
|
+
## Extensibility Interfaces
|
|
690
|
+
|
|
691
|
+
### SessionStrategy Interface
|
|
692
|
+
|
|
693
|
+
The `SessionStrategy` interface allows you to customize how OAuth tokens are converted into application sessions. The `tokenData` parameter is fully typed as `OAuthTokenResponse`:
|
|
694
|
+
|
|
695
|
+
```typescript
|
|
696
|
+
interface SessionStrategy {
|
|
697
|
+
createSession(tokenData: OAuthTokenResponse): Promise<Session>;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
interface Session<TRaw = OAuthTokenResponse> {
|
|
701
|
+
token: string; // Your session identifier
|
|
702
|
+
raw: TRaw; // Session data (defaults to OAuthTokenResponse)
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
interface OAuthTokenResponse {
|
|
706
|
+
access_token: string; // Required
|
|
707
|
+
token_type: string; // Required
|
|
708
|
+
expires_in?: number; // Optional
|
|
709
|
+
refresh_token?: string; // Optional
|
|
710
|
+
scope?: string; // Optional
|
|
711
|
+
id_token?: string; // Optional (OIDC)
|
|
712
|
+
[key: string]: unknown; // Provider-specific fields
|
|
713
|
+
}
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
**Use cases:**
|
|
717
|
+
- Decode ID tokens for user information
|
|
718
|
+
- Create or update users in your database
|
|
719
|
+
- Generate custom session identifiers
|
|
720
|
+
- Store tokens securely
|
|
721
|
+
- Add custom claims or metadata
|
|
722
|
+
|
|
723
|
+
**Example with database integration and proper typing:**
|
|
724
|
+
```typescript
|
|
725
|
+
import { SessionStrategy, OAuthTokenResponse, Session } from "@vunexa/lixa";
|
|
726
|
+
|
|
727
|
+
interface CustomSessionData extends OAuthTokenResponse {
|
|
728
|
+
userId: string;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const dbSessionStrategy: SessionStrategy = {
|
|
732
|
+
createSession: async (tokenData: OAuthTokenResponse): Promise<Session<CustomSessionData>> => {
|
|
733
|
+
// tokenData is properly typed with OAuth 2.0 fields
|
|
734
|
+
// TypeScript will catch errors like tokenData.id_tokn (typo)
|
|
735
|
+
|
|
736
|
+
// Decode ID token (for OIDC providers)
|
|
737
|
+
const payload = tokenData.id_token
|
|
738
|
+
? decodeJwt(tokenData.id_token)
|
|
739
|
+
: null;
|
|
740
|
+
|
|
741
|
+
// Create/update user
|
|
742
|
+
const user = await db.users.upsert({
|
|
743
|
+
email: payload?.email,
|
|
744
|
+
name: payload?.name
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
// Generate session
|
|
748
|
+
const sessionId = generateId();
|
|
749
|
+
await db.sessions.create({
|
|
750
|
+
id: sessionId,
|
|
751
|
+
userId: user.id,
|
|
752
|
+
accessToken: tokenData.access_token,
|
|
753
|
+
refreshToken: tokenData.refresh_token,
|
|
754
|
+
expiresAt: tokenData.expires_in
|
|
755
|
+
? new Date(Date.now() + tokenData.expires_in * 1000)
|
|
756
|
+
: null
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
return {
|
|
760
|
+
token: sessionId,
|
|
761
|
+
raw: {
|
|
762
|
+
...tokenData,
|
|
763
|
+
userId: user.id
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
### StateDao Interface
|
|
771
|
+
|
|
772
|
+
The `StateDao` interface manages OAuth state storage for CSRF protection and PKCE:
|
|
773
|
+
|
|
774
|
+
```typescript
|
|
775
|
+
interface StateDao {
|
|
776
|
+
saveState(state: string, data: StateData, expiresInSeconds: number): Promise<void>;
|
|
777
|
+
getState(state: string): Promise<StateData | null>;
|
|
778
|
+
deleteState(state: string): Promise<void>;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
interface StateData {
|
|
782
|
+
provider: string; // Provider name for routing
|
|
783
|
+
codeVerifier: string; // PKCE code verifier (64 hex chars)
|
|
784
|
+
createdAt: number; // Unix timestamp in milliseconds
|
|
785
|
+
}
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
**Required fields:**
|
|
789
|
+
- `provider`: Used to route callbacks to the correct provider configuration
|
|
790
|
+
- `codeVerifier`: Required for PKCE token exchange (RFC 7636)
|
|
791
|
+
- `createdAt`: Timestamp for debugging and validation
|
|
792
|
+
|
|
793
|
+
**Example with Redis:**
|
|
794
|
+
```typescript
|
|
795
|
+
const redisStateDao: StateDao = {
|
|
796
|
+
saveState: async (state, data, expiresInSeconds) => {
|
|
797
|
+
await redis.setex(
|
|
798
|
+
`oauth:state:${state}`,
|
|
799
|
+
expiresInSeconds,
|
|
800
|
+
JSON.stringify(data)
|
|
801
|
+
);
|
|
802
|
+
},
|
|
803
|
+
|
|
804
|
+
getState: async (state) => {
|
|
805
|
+
const data = await redis.get(`oauth:state:${state}`);
|
|
806
|
+
return data ? JSON.parse(data) : null;
|
|
807
|
+
},
|
|
808
|
+
|
|
809
|
+
deleteState: async (state) => {
|
|
810
|
+
await redis.del(`oauth:state:${state}`);
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
### SessionDao Interface
|
|
816
|
+
|
|
817
|
+
The `SessionDao` interface manages persistent user session storage with generic typing for flexibility:
|
|
818
|
+
|
|
819
|
+
```typescript
|
|
820
|
+
interface SessionDao {
|
|
821
|
+
saveSession<T = unknown>(sessionId: string, data: T, expiresInSeconds: number): Promise<void>;
|
|
822
|
+
getSession<T = unknown>(sessionId: string): Promise<T | null>;
|
|
823
|
+
deleteSession(sessionId: string): Promise<void>;
|
|
824
|
+
}
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
**Example with database and proper typing:**
|
|
828
|
+
```typescript
|
|
829
|
+
import { SessionDao, Session } from "@vunexa/lixa";
|
|
830
|
+
|
|
831
|
+
const dbSessionDao: SessionDao = {
|
|
832
|
+
saveSession: async <T = unknown>(sessionId: string, data: T, expiresInSeconds: number) => {
|
|
833
|
+
const expiresAt = new Date(Date.now() + expiresInSeconds * 1000);
|
|
834
|
+
await db.sessions.create({
|
|
835
|
+
id: sessionId,
|
|
836
|
+
data: JSON.stringify(data),
|
|
837
|
+
expiresAt
|
|
838
|
+
});
|
|
839
|
+
},
|
|
840
|
+
|
|
841
|
+
getSession: async <T = unknown>(sessionId: string): Promise<T | null> => {
|
|
842
|
+
const session = await db.sessions.findOne({
|
|
843
|
+
id: sessionId,
|
|
844
|
+
expiresAt: { $gt: new Date() }
|
|
845
|
+
});
|
|
846
|
+
return session ? JSON.parse(session.data) as T : null;
|
|
847
|
+
},
|
|
848
|
+
|
|
849
|
+
deleteSession: async (sessionId: string) => {
|
|
850
|
+
await db.sessions.delete({ id: sessionId });
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
## PKCE Implementation
|
|
856
|
+
|
|
857
|
+
Lixa automatically implements **PKCE (Proof Key for Code Exchange)** according to [RFC 7636](https://tools.ietf.org/html/rfc7636) for all OAuth flows.
|
|
858
|
+
|
|
859
|
+
### How it Works
|
|
860
|
+
|
|
861
|
+
1. **Code Verifier Generation**
|
|
862
|
+
- Generates 32 cryptographically random bytes
|
|
863
|
+
- Encodes as 64-character hexadecimal string
|
|
864
|
+
- Example: `a1b2c3d4e5f6...` (64 chars)
|
|
865
|
+
|
|
866
|
+
2. **Code Challenge Generation**
|
|
867
|
+
- Creates SHA-256 hash of the code verifier
|
|
868
|
+
- Encodes as base64url (RFC 4648)
|
|
869
|
+
- Sends with authorization request
|
|
870
|
+
|
|
871
|
+
3. **Token Exchange**
|
|
872
|
+
- Retrieves code verifier from state storage
|
|
873
|
+
- Sends original code verifier with token request
|
|
874
|
+
- Provider validates: `SHA256(code_verifier) === code_challenge`
|
|
875
|
+
|
|
876
|
+
### Security Benefits
|
|
877
|
+
|
|
878
|
+
- **Prevents authorization code interception**: Even if an attacker intercepts the authorization code, they cannot exchange it for tokens without the code verifier
|
|
879
|
+
- **No client secret required**: PKCE works without client secrets, making it suitable for public clients
|
|
880
|
+
- **Standards compliant**: Follows RFC 7636 specification
|
|
881
|
+
|
|
882
|
+
### Debug Logging
|
|
883
|
+
|
|
884
|
+
Enable debug mode to see PKCE flow details:
|
|
885
|
+
|
|
886
|
+
```typescript
|
|
887
|
+
const lixa = new Lixa({
|
|
888
|
+
providers: { /* ... */ },
|
|
889
|
+
debug: true
|
|
890
|
+
});
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
Debug output includes:
|
|
894
|
+
- `[Lixa] [timestamp] [INFO] [Auth]` - Code verifier and challenge generation
|
|
895
|
+
- `[Lixa] [timestamp] [INFO] [State]` - State storage with code verifier
|
|
896
|
+
- `[Lixa] [timestamp] [INFO] [Token]` - Token exchange with code verifier
|
|
897
|
+
|
|
898
|
+
## Migration Guide
|
|
899
|
+
|
|
900
|
+
### Migrating from Legacy Registration Pattern
|
|
901
|
+
|
|
902
|
+
**Old Pattern (Deprecated):**
|
|
903
|
+
```typescript
|
|
904
|
+
import { Lixa, IProvider } from "@vunexa/lixa";
|
|
905
|
+
import { GoogleProvider } from "@vunexa/lixa";
|
|
906
|
+
|
|
907
|
+
// Step 1: Register providers globally
|
|
908
|
+
Lixa.registerProvider({
|
|
909
|
+
google: new GoogleProvider()
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
// Step 2: Configure with credentials only
|
|
913
|
+
const lixa = new Lixa({
|
|
914
|
+
providers: {
|
|
915
|
+
google: {
|
|
916
|
+
clientId: "...",
|
|
917
|
+
clientSecret: "...",
|
|
918
|
+
redirectUri: "...",
|
|
919
|
+
scopes: ["openid", "email"]
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
});
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
**New Pattern (Recommended):**
|
|
926
|
+
```typescript
|
|
927
|
+
import { Lixa } from "@vunexa/lixa";
|
|
928
|
+
import { GoogleProvider } from "@vunexa/lixa-providers";
|
|
929
|
+
|
|
930
|
+
// Single step: Pass provider inline
|
|
931
|
+
const lixa = new Lixa({
|
|
932
|
+
providers: {
|
|
933
|
+
google: {
|
|
934
|
+
provider: new GoogleProvider(), // ← Add this line
|
|
935
|
+
clientId: "...",
|
|
936
|
+
clientSecret: "...",
|
|
937
|
+
redirectUri: "...",
|
|
938
|
+
scopes: ["openid", "email"]
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
});
|
|
942
|
+
```
|
|
943
|
+
|
|
944
|
+
**Migration Steps:**
|
|
945
|
+
1. Install `@vunexa/lixa-providers`: `npm install @vunexa/lixa-providers`
|
|
946
|
+
2. Update imports: `import { GoogleProvider } from "@vunexa/lixa-providers"`
|
|
947
|
+
3. Remove `Lixa.registerProvider()` calls
|
|
948
|
+
4. Add `provider` field to each provider configuration
|
|
949
|
+
5. Test your OAuth flows
|
|
950
|
+
|
|
951
|
+
**Benefits:**
|
|
952
|
+
- ✅ No global state - each Lixa instance is independent
|
|
953
|
+
- ✅ Better testability - easy to mock providers per instance
|
|
954
|
+
- ✅ Clearer configuration - everything in one place
|
|
955
|
+
- ✅ Type safety - TypeScript knows which providers are configured
|
|
473
956
|
|
|
474
957
|
## Security Considerations
|
|
475
958
|
|