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