@lenne.tech/nest-server 11.7.1 → 11.7.2
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/dist/core/common/interfaces/server-options.interface.d.ts +18 -15
- package/dist/core/modules/auth/core-auth.controller.js +2 -2
- package/dist/core/modules/auth/core-auth.controller.js.map +1 -1
- package/dist/core/modules/auth/core-auth.resolver.js +2 -2
- package/dist/core/modules/auth/core-auth.resolver.js.map +1 -1
- package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js +1 -1
- package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth-rate-limiter.service.js +1 -1
- package/dist/core/modules/better-auth/better-auth-rate-limiter.service.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth-user.mapper.js +7 -55
- package/dist/core/modules/better-auth/better-auth-user.mapper.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.config.js +29 -10
- package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.middleware.d.ts +1 -0
- package/dist/core/modules/better-auth/better-auth.middleware.js +55 -1
- package/dist/core/modules/better-auth/better-auth.middleware.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.module.d.ts +1 -1
- package/dist/core/modules/better-auth/better-auth.module.js +46 -18
- package/dist/core/modules/better-auth/better-auth.module.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.resolver.js +0 -11
- package/dist/core/modules/better-auth/better-auth.resolver.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.service.d.ts +22 -1
- package/dist/core/modules/better-auth/better-auth.service.js +209 -8
- package/dist/core/modules/better-auth/better-auth.service.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.types.d.ts +2 -0
- package/dist/core/modules/better-auth/better-auth.types.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.resolver.d.ts +5 -0
- package/dist/core/modules/better-auth/core-better-auth.resolver.js +58 -12
- package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -1
- package/dist/core.module.js +6 -3
- package/dist/core.module.js.map +1 -1
- package/dist/server/modules/better-auth/better-auth.module.d.ts +1 -1
- package/dist/server/modules/better-auth/better-auth.module.js +2 -1
- package/dist/server/modules/better-auth/better-auth.module.js.map +1 -1
- package/dist/server/modules/better-auth/better-auth.resolver.d.ts +3 -0
- package/dist/server/modules/better-auth/better-auth.resolver.js +14 -11
- package/dist/server/modules/better-auth/better-auth.resolver.js.map +1 -1
- package/dist/server/modules/user/user.controller.js +0 -8
- package/dist/server/modules/user/user.controller.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/core/common/interfaces/server-options.interface.ts +129 -58
- package/src/core/modules/auth/core-auth.controller.ts +2 -2
- package/src/core/modules/auth/core-auth.resolver.ts +2 -2
- package/src/core/modules/auth/services/legacy-auth-rate-limiter.service.ts +1 -1
- package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +12 -11
- package/src/core/modules/better-auth/README.md +82 -43
- package/src/core/modules/better-auth/better-auth-rate-limiter.service.ts +1 -1
- package/src/core/modules/better-auth/better-auth-user.mapper.ts +9 -77
- package/src/core/modules/better-auth/better-auth.config.ts +45 -15
- package/src/core/modules/better-auth/better-auth.middleware.ts +85 -2
- package/src/core/modules/better-auth/better-auth.module.ts +83 -27
- package/src/core/modules/better-auth/better-auth.resolver.ts +0 -11
- package/src/core/modules/better-auth/better-auth.service.ts +367 -12
- package/src/core/modules/better-auth/better-auth.types.ts +16 -0
- package/src/core/modules/better-auth/core-better-auth.resolver.ts +111 -16
- package/src/core.module.ts +9 -3
- package/src/server/modules/better-auth/better-auth.module.ts +9 -3
- package/src/server/modules/better-auth/better-auth.resolver.ts +9 -11
- package/src/server/modules/user/user.controller.ts +1 -9
|
@@ -243,7 +243,6 @@ export class BetterAuthUserMapper {
|
|
|
243
243
|
}
|
|
244
244
|
|
|
245
245
|
if (!plainPassword) {
|
|
246
|
-
this.logger.debug('No plain password provided - cannot create bcrypt hash for legacy');
|
|
247
246
|
return false;
|
|
248
247
|
}
|
|
249
248
|
|
|
@@ -266,13 +265,7 @@ export class BetterAuthUserMapper {
|
|
|
266
265
|
{ $set: { password: bcryptHash, updatedAt: new Date() } },
|
|
267
266
|
);
|
|
268
267
|
|
|
269
|
-
|
|
270
|
-
this.logger.debug(`Created bcrypt hash for legacy auth for user ${userEmail}`);
|
|
271
|
-
return true;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
this.logger.debug(`No user found to sync password for ${userEmail}`);
|
|
275
|
-
return false;
|
|
268
|
+
return result.modifiedCount > 0;
|
|
276
269
|
} catch (error) {
|
|
277
270
|
this.logger.error(
|
|
278
271
|
`Error syncing password to legacy: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
@@ -304,7 +297,6 @@ export class BetterAuthUserMapper {
|
|
|
304
297
|
}
|
|
305
298
|
|
|
306
299
|
if (!plainPassword) {
|
|
307
|
-
this.logger.debug('No plain password provided - cannot sync to IAM');
|
|
308
300
|
return false;
|
|
309
301
|
}
|
|
310
302
|
|
|
@@ -315,7 +307,6 @@ export class BetterAuthUserMapper {
|
|
|
315
307
|
// Find the user
|
|
316
308
|
const user = await usersCollection.findOne({ email: userEmail });
|
|
317
309
|
if (!user) {
|
|
318
|
-
this.logger.debug(`No user found with email ${userEmail} - cannot sync password to IAM`);
|
|
319
310
|
return false;
|
|
320
311
|
}
|
|
321
312
|
|
|
@@ -326,7 +317,6 @@ export class BetterAuthUserMapper {
|
|
|
326
317
|
});
|
|
327
318
|
|
|
328
319
|
if (!existingAccount) {
|
|
329
|
-
this.logger.debug(`No IAM credential account found for ${userEmail} - sync not needed`);
|
|
330
320
|
return false;
|
|
331
321
|
}
|
|
332
322
|
|
|
@@ -334,7 +324,7 @@ export class BetterAuthUserMapper {
|
|
|
334
324
|
const scryptHash = await this.hashPasswordForBetterAuth(plainPassword);
|
|
335
325
|
|
|
336
326
|
// Update the account password
|
|
337
|
-
|
|
327
|
+
await accountCollection.updateOne(
|
|
338
328
|
{ _id: existingAccount._id },
|
|
339
329
|
{
|
|
340
330
|
$set: {
|
|
@@ -344,12 +334,6 @@ export class BetterAuthUserMapper {
|
|
|
344
334
|
},
|
|
345
335
|
);
|
|
346
336
|
|
|
347
|
-
if (result.modifiedCount > 0) {
|
|
348
|
-
this.logger.debug(`Synced password change to IAM for user ${userEmail}`);
|
|
349
|
-
return true;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
this.logger.debug(`Password already up to date in IAM for ${userEmail}`);
|
|
353
337
|
return true;
|
|
354
338
|
} catch (error) {
|
|
355
339
|
this.logger.error(`Error syncing password to IAM: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
@@ -371,7 +355,6 @@ export class BetterAuthUserMapper {
|
|
|
371
355
|
*/
|
|
372
356
|
async migrateAccountToIam(userEmail: string, plainPassword?: string): Promise<boolean> {
|
|
373
357
|
if (!this.connection) {
|
|
374
|
-
this.logger.warn('No database connection available - cannot migrate account to IAM');
|
|
375
358
|
return false;
|
|
376
359
|
}
|
|
377
360
|
|
|
@@ -383,7 +366,6 @@ export class BetterAuthUserMapper {
|
|
|
383
366
|
const legacyUser = await usersCollection.findOne({ email: userEmail });
|
|
384
367
|
|
|
385
368
|
if (!legacyUser?.password) {
|
|
386
|
-
this.logger.debug(`No legacy user with password found for ${userEmail}`);
|
|
387
369
|
return false;
|
|
388
370
|
}
|
|
389
371
|
|
|
@@ -396,17 +378,12 @@ export class BetterAuthUserMapper {
|
|
|
396
378
|
const directMatch = await bcrypt.compare(plainPassword, legacyUser.password);
|
|
397
379
|
const sha256Match = await bcrypt.compare(sha256(plainPassword), legacyUser.password);
|
|
398
380
|
if (!directMatch && !sha256Match) {
|
|
399
|
-
// Security: Wrong password provided for migration - reject
|
|
400
|
-
this.logger.warn(
|
|
401
|
-
`SECURITY: Password verification failed for ${userEmail} - migration rejected. This may indicate an attempted unauthorized migration.`,
|
|
402
|
-
);
|
|
403
|
-
// Return false instead of throwing to avoid exposing user existence
|
|
404
|
-
// The calling code (CoreAuthService) will handle this as authentication failure
|
|
381
|
+
// Security: Wrong password provided for migration - reject
|
|
382
|
+
this.logger.warn(`Migration password verification failed for ${userEmail}`);
|
|
405
383
|
return false;
|
|
406
384
|
}
|
|
407
385
|
} else {
|
|
408
386
|
// No password provided - cannot verify, cannot migrate
|
|
409
|
-
this.logger.debug(`No password provided for ${userEmail} - migration skipped`);
|
|
410
387
|
return false;
|
|
411
388
|
}
|
|
412
389
|
|
|
@@ -433,8 +410,6 @@ export class BetterAuthUserMapper {
|
|
|
433
410
|
},
|
|
434
411
|
},
|
|
435
412
|
);
|
|
436
|
-
|
|
437
|
-
this.logger.debug(`Added Better-Auth id ${stringId} to legacy user ${userEmail}`);
|
|
438
413
|
}
|
|
439
414
|
|
|
440
415
|
// Check if credential account already exists
|
|
@@ -445,23 +420,11 @@ export class BetterAuthUserMapper {
|
|
|
445
420
|
});
|
|
446
421
|
|
|
447
422
|
if (existingAccount) {
|
|
448
|
-
this.logger.debug(`Credential account already exists for ${userEmail}`);
|
|
449
423
|
return true;
|
|
450
424
|
}
|
|
451
425
|
|
|
452
|
-
// Create the credential account
|
|
453
|
-
|
|
454
|
-
// Otherwise, migration is not possible (legacy bcrypt hash is not compatible)
|
|
455
|
-
let passwordHash: string;
|
|
456
|
-
if (plainPassword) {
|
|
457
|
-
// Better-Auth uses scrypt with format: salt:hash (both hex encoded)
|
|
458
|
-
passwordHash = await this.hashPasswordForBetterAuth(plainPassword);
|
|
459
|
-
this.logger.debug(`Created Better-Auth compatible scrypt hash for ${userEmail}`);
|
|
460
|
-
} else {
|
|
461
|
-
// Without plain password, we cannot migrate - Better-Auth uses scrypt, Legacy uses bcrypt
|
|
462
|
-
this.logger.warn(`Cannot migrate ${userEmail} without plain password - hash formats are incompatible`);
|
|
463
|
-
return false;
|
|
464
|
-
}
|
|
426
|
+
// Create the credential account with Better-Auth compatible scrypt hash
|
|
427
|
+
const passwordHash = await this.hashPasswordForBetterAuth(plainPassword);
|
|
465
428
|
|
|
466
429
|
const now = new Date();
|
|
467
430
|
// Store account matching Better-Auth's format:
|
|
@@ -477,7 +440,6 @@ export class BetterAuthUserMapper {
|
|
|
477
440
|
userId: userMongoId,
|
|
478
441
|
});
|
|
479
442
|
|
|
480
|
-
this.logger.debug(`Created IAM credential account for legacy user ${userEmail}`);
|
|
481
443
|
return true;
|
|
482
444
|
} catch (error) {
|
|
483
445
|
this.logger.error(`Error migrating account to IAM: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
@@ -685,22 +647,15 @@ export class BetterAuthUserMapper {
|
|
|
685
647
|
const user = await usersCollection.findOne({ email: newEmail });
|
|
686
648
|
|
|
687
649
|
if (!user) {
|
|
688
|
-
this.logger.debug(`No user found with new email ${newEmail}`);
|
|
689
650
|
return false;
|
|
690
651
|
}
|
|
691
652
|
|
|
692
653
|
// Invalidate all existing sessions for this user
|
|
693
654
|
// This forces re-authentication with the new email
|
|
694
655
|
if (user._id) {
|
|
695
|
-
|
|
696
|
-
if (deleteResult.deletedCount > 0) {
|
|
697
|
-
this.logger.debug(
|
|
698
|
-
`Invalidated ${deleteResult.deletedCount} sessions after email change for user ${newEmail}`,
|
|
699
|
-
);
|
|
700
|
-
}
|
|
656
|
+
await sessionCollection.deleteMany({ userId: user._id });
|
|
701
657
|
}
|
|
702
658
|
|
|
703
|
-
this.logger.debug(`Email change synced from Legacy to IAM: ${oldEmail} → ${newEmail}`);
|
|
704
659
|
return true;
|
|
705
660
|
} catch (error) {
|
|
706
661
|
this.logger.error(
|
|
@@ -744,13 +699,7 @@ export class BetterAuthUserMapper {
|
|
|
744
699
|
{ returnDocument: 'after' },
|
|
745
700
|
);
|
|
746
701
|
|
|
747
|
-
|
|
748
|
-
this.logger.debug(`No user found with iamId ${userId}`);
|
|
749
|
-
return false;
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
this.logger.debug(`Email change synced from IAM to Legacy for user ${userId}: → ${newEmail}`);
|
|
753
|
-
return true;
|
|
702
|
+
return !!result;
|
|
754
703
|
} catch (error) {
|
|
755
704
|
this.logger.error(
|
|
756
705
|
`Error syncing email change from IAM: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
@@ -807,7 +756,6 @@ export class BetterAuthUserMapper {
|
|
|
807
756
|
}
|
|
808
757
|
|
|
809
758
|
if (!user) {
|
|
810
|
-
this.logger.debug(`No user found with identifier ${userIdentifier}`);
|
|
811
759
|
return { accountsDeleted: 0, sessionsDeleted: 0, success: false, userDeleted: false };
|
|
812
760
|
}
|
|
813
761
|
|
|
@@ -874,12 +822,6 @@ export class BetterAuthUserMapper {
|
|
|
874
822
|
const accountsResult = await accountCollection.deleteMany({ userId: userObjectId });
|
|
875
823
|
const accountsDeleted = accountsResult.deletedCount;
|
|
876
824
|
|
|
877
|
-
if (sessionsDeleted > 0 || accountsDeleted > 0) {
|
|
878
|
-
this.logger.debug(
|
|
879
|
-
`Cleaned up IAM data for user ${userId}: accounts=${accountsDeleted}, sessions=${sessionsDeleted}`,
|
|
880
|
-
);
|
|
881
|
-
}
|
|
882
|
-
|
|
883
825
|
return {
|
|
884
826
|
accountsDeleted,
|
|
885
827
|
sessionsDeleted,
|
|
@@ -914,13 +856,7 @@ export class BetterAuthUserMapper {
|
|
|
914
856
|
$or: [{ iamId: iamUserId }, { id: iamUserId }],
|
|
915
857
|
});
|
|
916
858
|
|
|
917
|
-
|
|
918
|
-
this.logger.debug(`Cleaned up Legacy user for IAM user ${iamUserId}`);
|
|
919
|
-
return true;
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
this.logger.debug(`No Legacy user found for IAM user ${iamUserId}`);
|
|
923
|
-
return false;
|
|
859
|
+
return result.deletedCount > 0;
|
|
924
860
|
} catch (error) {
|
|
925
861
|
this.logger.error(`Error cleaning up Legacy data: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
926
862
|
return false;
|
|
@@ -1043,10 +979,6 @@ export class BetterAuthUserMapper {
|
|
|
1043
979
|
// Can disable legacy auth only if ALL users are fully migrated
|
|
1044
980
|
const canDisableLegacyAuth = totalUsers > 0 && fullyMigratedUsers === totalUsers;
|
|
1045
981
|
|
|
1046
|
-
this.logger.debug(
|
|
1047
|
-
`Migration status: ${fullyMigratedUsers}/${totalUsers} users migrated (${migrationPercentage}%)`,
|
|
1048
|
-
);
|
|
1049
|
-
|
|
1050
982
|
return {
|
|
1051
983
|
canDisableLegacyAuth,
|
|
1052
984
|
fullyMigratedUsers,
|
|
@@ -168,42 +168,48 @@ export function createBetterAuthInstance(options: CreateBetterAuthOptions): Bett
|
|
|
168
168
|
* Builds the plugins array based on configuration.
|
|
169
169
|
* Merges built-in plugins (jwt, twoFactor, passkey) with custom plugins from config.
|
|
170
170
|
*
|
|
171
|
-
* Plugins
|
|
172
|
-
*
|
|
171
|
+
* Plugins accept both boolean and object configuration:
|
|
172
|
+
* - `true` or `{}`: Enable with defaults
|
|
173
|
+
* - `{ option: value }`: Enable with custom settings
|
|
174
|
+
* - `false` or `{ enabled: false }`: Disable
|
|
175
|
+
* - `undefined`: Disabled (default)
|
|
173
176
|
*/
|
|
174
177
|
function buildPlugins(config: IBetterAuth): BetterAuthPlugin[] {
|
|
175
178
|
const plugins: BetterAuthPlugin[] = [];
|
|
176
179
|
|
|
177
180
|
// JWT Plugin for API client compatibility
|
|
178
|
-
//
|
|
179
|
-
|
|
181
|
+
// JWT is enabled by default unless explicitly disabled (jwt: false or jwt: { enabled: false })
|
|
182
|
+
const jwtExplicitlyDisabled =
|
|
183
|
+
config.jwt === false || (typeof config.jwt === 'object' && config.jwt?.enabled === false);
|
|
184
|
+
if (!jwtExplicitlyDisabled) {
|
|
185
|
+
const jwtConfig = typeof config.jwt === 'object' ? config.jwt : {};
|
|
180
186
|
plugins.push(
|
|
181
187
|
jwt({
|
|
182
188
|
jwt: {
|
|
183
|
-
expirationTime:
|
|
189
|
+
expirationTime: jwtConfig.expiresIn || '15m',
|
|
184
190
|
},
|
|
185
191
|
}),
|
|
186
192
|
);
|
|
187
193
|
}
|
|
188
194
|
|
|
189
195
|
// Two-Factor Authentication Plugin
|
|
190
|
-
|
|
191
|
-
if (
|
|
196
|
+
const twoFactorConfig = getPluginConfig(config.twoFactor);
|
|
197
|
+
if (twoFactorConfig) {
|
|
192
198
|
plugins.push(
|
|
193
199
|
twoFactor({
|
|
194
|
-
issuer:
|
|
200
|
+
issuer: twoFactorConfig.appName || 'Nest Server',
|
|
195
201
|
}),
|
|
196
202
|
);
|
|
197
203
|
}
|
|
198
204
|
|
|
199
205
|
// Passkey/WebAuthn Plugin
|
|
200
|
-
|
|
201
|
-
if (
|
|
206
|
+
const passkeyConfig = getPluginConfig(config.passkey);
|
|
207
|
+
if (passkeyConfig) {
|
|
202
208
|
plugins.push(
|
|
203
209
|
passkey({
|
|
204
|
-
origin:
|
|
205
|
-
rpID:
|
|
206
|
-
rpName:
|
|
210
|
+
origin: passkeyConfig.origin || 'http://localhost:3000',
|
|
211
|
+
rpID: passkeyConfig.rpId || 'localhost',
|
|
212
|
+
rpName: passkeyConfig.rpName || 'Nest Server',
|
|
207
213
|
}),
|
|
208
214
|
);
|
|
209
215
|
}
|
|
@@ -322,6 +328,29 @@ function getAutoGeneratedSecret(): string {
|
|
|
322
328
|
return cachedAutoGeneratedSecret;
|
|
323
329
|
}
|
|
324
330
|
|
|
331
|
+
/**
|
|
332
|
+
* Gets plugin configuration as object, handling boolean shorthand.
|
|
333
|
+
* Returns undefined if disabled, or the config object if enabled.
|
|
334
|
+
*/
|
|
335
|
+
function getPluginConfig<T extends { enabled?: boolean }>(config: boolean | T | undefined): T | undefined {
|
|
336
|
+
if (!isPluginEnabled(config)) return undefined;
|
|
337
|
+
if (typeof config === 'boolean') return {} as T;
|
|
338
|
+
return config;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Checks if a plugin configuration is enabled.
|
|
343
|
+
* Supports both boolean and object configuration:
|
|
344
|
+
* - `true` or `{}` or `{ enabled: true }` → enabled
|
|
345
|
+
* - `false` or `{ enabled: false }` → disabled
|
|
346
|
+
* - `undefined` → disabled
|
|
347
|
+
*/
|
|
348
|
+
function isPluginEnabled<T extends { enabled?: boolean }>(config: boolean | T | undefined): boolean {
|
|
349
|
+
if (config === undefined) return false;
|
|
350
|
+
if (typeof config === 'boolean') return config;
|
|
351
|
+
return config.enabled !== false;
|
|
352
|
+
}
|
|
353
|
+
|
|
325
354
|
/**
|
|
326
355
|
* Checks if a secret has valid minimum length (32 characters)
|
|
327
356
|
*/
|
|
@@ -416,8 +445,9 @@ function validateConfig(config: IBetterAuth, fallbackSecrets?: (string | undefin
|
|
|
416
445
|
}
|
|
417
446
|
|
|
418
447
|
// Validate passkey origin
|
|
419
|
-
|
|
420
|
-
|
|
448
|
+
const passkeyConfig = typeof config.passkey === 'object' ? config.passkey : null;
|
|
449
|
+
if (passkeyConfig?.enabled && passkeyConfig.origin && !isValidUrl(passkeyConfig.origin)) {
|
|
450
|
+
errors.push(`Invalid passkey origin format: ${passkeyConfig.origin}`);
|
|
421
451
|
}
|
|
422
452
|
|
|
423
453
|
// Validate social providers dynamically
|
|
@@ -49,7 +49,7 @@ export class BetterAuthMiddleware implements NestMiddleware {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
try {
|
|
52
|
-
//
|
|
52
|
+
// Strategy 1: Try session-based authentication (cookies)
|
|
53
53
|
const session = await this.getSession(req);
|
|
54
54
|
|
|
55
55
|
if (session?.user) {
|
|
@@ -63,7 +63,69 @@ export class BetterAuthMiddleware implements NestMiddleware {
|
|
|
63
63
|
if (mappedUser) {
|
|
64
64
|
// Attach the mapped user to the request
|
|
65
65
|
// This makes it compatible with @CurrentUser() and RolesGuard
|
|
66
|
-
|
|
66
|
+
// Set _authenticatedViaBetterAuth flag so AuthGuard skips Passport JWT verification
|
|
67
|
+
req.user = { ...mappedUser, _authenticatedViaBetterAuth: true };
|
|
68
|
+
return next();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Strategy 2: Try Authorization header (Bearer token)
|
|
73
|
+
// The token could be a BetterAuth JWT, a Legacy JWT, or a session token
|
|
74
|
+
if (req.headers.authorization) {
|
|
75
|
+
const authHeader = req.headers.authorization;
|
|
76
|
+
const token = authHeader.startsWith('Bearer ') ? authHeader.substring(7) : authHeader;
|
|
77
|
+
const tokenParts = token.split('.').length;
|
|
78
|
+
|
|
79
|
+
// Check if token looks like a JWT (has 3 parts)
|
|
80
|
+
if (tokenParts === 3) {
|
|
81
|
+
// Decode JWT payload to check if it's a Legacy JWT or BetterAuth JWT
|
|
82
|
+
// Legacy JWTs have 'id' claim, BetterAuth JWTs have 'sub' claim
|
|
83
|
+
const isLegacyJwt = this.isLegacyJwt(token);
|
|
84
|
+
if (isLegacyJwt) {
|
|
85
|
+
// Legacy JWT - skip BetterAuth processing, let Passport handle it
|
|
86
|
+
return next();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Try BetterAuth JWT verification
|
|
90
|
+
if (this.betterAuthService.isJwtEnabled()) {
|
|
91
|
+
const jwtPayload = await this.betterAuthService.verifyJwtFromRequest(req);
|
|
92
|
+
|
|
93
|
+
if (jwtPayload?.sub) {
|
|
94
|
+
// JWT payload contains user info - create a session-like user object
|
|
95
|
+
const sessionUser: BetterAuthSessionUser = {
|
|
96
|
+
email: jwtPayload.email || '',
|
|
97
|
+
emailVerified: jwtPayload.emailVerified,
|
|
98
|
+
id: jwtPayload.sub,
|
|
99
|
+
name: jwtPayload.name,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
req.betterAuthUser = sessionUser;
|
|
103
|
+
|
|
104
|
+
// Map the JWT user to our User model with hasRole()
|
|
105
|
+
const mappedUser = await this.userMapper.mapSessionUser(sessionUser);
|
|
106
|
+
|
|
107
|
+
if (mappedUser) {
|
|
108
|
+
req.user = { ...mappedUser, _authenticatedViaBetterAuth: true };
|
|
109
|
+
return next();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// If user is still not set, try session token verification as fallback
|
|
116
|
+
// This handles both non-JWT tokens and JWTs that couldn't be verified
|
|
117
|
+
if (!req.user) {
|
|
118
|
+
const sessionResult = await this.betterAuthService.getSessionByToken(token);
|
|
119
|
+
if (sessionResult?.user) {
|
|
120
|
+
req.betterAuthSession = { session: sessionResult.session, user: sessionResult.user };
|
|
121
|
+
req.betterAuthUser = sessionResult.user;
|
|
122
|
+
|
|
123
|
+
const mappedUser = await this.userMapper.mapSessionUser(sessionResult.user);
|
|
124
|
+
if (mappedUser) {
|
|
125
|
+
req.user = { ...mappedUser, _authenticatedViaBetterAuth: true };
|
|
126
|
+
return next();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
67
129
|
}
|
|
68
130
|
}
|
|
69
131
|
} catch (error) {
|
|
@@ -75,6 +137,27 @@ export class BetterAuthMiddleware implements NestMiddleware {
|
|
|
75
137
|
next();
|
|
76
138
|
}
|
|
77
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Checks if a JWT token is a Legacy Auth JWT (has 'id' claim but no 'sub' claim)
|
|
142
|
+
* Legacy JWTs use 'id' for user ID, BetterAuth JWTs use 'sub'
|
|
143
|
+
*/
|
|
144
|
+
private isLegacyJwt(token: string): boolean {
|
|
145
|
+
try {
|
|
146
|
+
const parts = token.split('.');
|
|
147
|
+
if (parts.length !== 3) return false;
|
|
148
|
+
|
|
149
|
+
// Decode the payload (second part)
|
|
150
|
+
const payloadStr = Buffer.from(parts[1], 'base64url').toString('utf-8');
|
|
151
|
+
const payload = JSON.parse(payloadStr);
|
|
152
|
+
|
|
153
|
+
// Legacy JWT has 'id' claim (and typically 'deviceId', 'tokenId')
|
|
154
|
+
// BetterAuth JWT has 'sub' claim
|
|
155
|
+
return payload.id !== undefined && payload.sub === undefined;
|
|
156
|
+
} catch {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
78
161
|
/**
|
|
79
162
|
* Gets the session from Better-Auth
|
|
80
163
|
*/
|
|
@@ -20,7 +20,7 @@ import { BetterAuthUserMapper } from './better-auth-user.mapper';
|
|
|
20
20
|
import { BetterAuthInstance, createBetterAuthInstance } from './better-auth.config';
|
|
21
21
|
import { BetterAuthMiddleware } from './better-auth.middleware';
|
|
22
22
|
import { BetterAuthResolver } from './better-auth.resolver';
|
|
23
|
-
import { BetterAuthService } from './better-auth.service';
|
|
23
|
+
import { BETTER_AUTH_CONFIG, BetterAuthService } from './better-auth.service';
|
|
24
24
|
import { CoreBetterAuthController } from './core-better-auth.controller';
|
|
25
25
|
import { CoreBetterAuthResolver } from './core-better-auth.resolver';
|
|
26
26
|
|
|
@@ -34,9 +34,13 @@ export const BETTER_AUTH_INSTANCE = 'BETTER_AUTH_INSTANCE';
|
|
|
34
34
|
*/
|
|
35
35
|
export interface BetterAuthModuleOptions {
|
|
36
36
|
/**
|
|
37
|
-
* Better-auth configuration
|
|
37
|
+
* Better-auth configuration.
|
|
38
|
+
* Accepts:
|
|
39
|
+
* - `true`: Enable with all defaults (including JWT)
|
|
40
|
+
* - `false`: Disable BetterAuth
|
|
41
|
+
* - `{ ... }`: Enable with custom configuration
|
|
38
42
|
*/
|
|
39
|
-
config: IBetterAuth;
|
|
43
|
+
config: boolean | IBetterAuth;
|
|
40
44
|
|
|
41
45
|
/**
|
|
42
46
|
* Custom controller class to use instead of the default CoreBetterAuthController.
|
|
@@ -100,6 +104,20 @@ export interface BetterAuthModuleOptions {
|
|
|
100
104
|
resolver?: Type<CoreBetterAuthResolver>;
|
|
101
105
|
}
|
|
102
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Normalizes betterAuth config from boolean | IBetterAuth to IBetterAuth | null
|
|
109
|
+
* - `true` → `{}` (enabled with defaults)
|
|
110
|
+
* - `false` → `null` (disabled)
|
|
111
|
+
* - `undefined` → `null` (disabled for backward compatibility)
|
|
112
|
+
* - `{ ... }` → `{ ... }` (pass through)
|
|
113
|
+
*/
|
|
114
|
+
function normalizeBetterAuthConfig(config: boolean | IBetterAuth | undefined): IBetterAuth | null {
|
|
115
|
+
if (config === undefined || config === null) return null;
|
|
116
|
+
if (config === true) return {};
|
|
117
|
+
if (config === false) return null;
|
|
118
|
+
return config;
|
|
119
|
+
}
|
|
120
|
+
|
|
103
121
|
/**
|
|
104
122
|
* BetterAuthModule provides integration with the better-auth authentication framework.
|
|
105
123
|
*
|
|
@@ -178,12 +196,12 @@ export class BetterAuthModule implements NestModule, OnModuleInit {
|
|
|
178
196
|
// Apply rate limiting to Better-Auth endpoints only
|
|
179
197
|
if (BetterAuthModule.currentConfig?.rateLimit?.enabled) {
|
|
180
198
|
consumer.apply(BetterAuthRateLimitMiddleware).forRoutes(`${basePath}/*path`);
|
|
181
|
-
BetterAuthModule.logger.
|
|
199
|
+
BetterAuthModule.logger.debug(`Rate limiting middleware registered for ${basePath}/*path endpoints`);
|
|
182
200
|
}
|
|
183
201
|
|
|
184
202
|
// Apply session middleware to all routes
|
|
185
203
|
consumer.apply(BetterAuthMiddleware).forRoutes('(.*)'); // New path-to-regexp syntax for wildcard
|
|
186
|
-
BetterAuthModule.logger.
|
|
204
|
+
BetterAuthModule.logger.debug('BetterAuthMiddleware registered for all routes');
|
|
187
205
|
}
|
|
188
206
|
}
|
|
189
207
|
|
|
@@ -224,7 +242,10 @@ export class BetterAuthModule implements NestModule, OnModuleInit {
|
|
|
224
242
|
* @returns Dynamic module configuration
|
|
225
243
|
*/
|
|
226
244
|
static forRoot(options: BetterAuthModuleOptions): DynamicModule {
|
|
227
|
-
const { config, controller, fallbackSecrets, resolver } = options;
|
|
245
|
+
const { config: rawConfig, controller, fallbackSecrets, resolver } = options;
|
|
246
|
+
|
|
247
|
+
// Normalize config: true → {}, false/undefined → null
|
|
248
|
+
const config = normalizeBetterAuthConfig(rawConfig);
|
|
228
249
|
|
|
229
250
|
// Store config for middleware configuration
|
|
230
251
|
this.currentConfig = config;
|
|
@@ -233,12 +254,11 @@ export class BetterAuthModule implements NestModule, OnModuleInit {
|
|
|
233
254
|
// Store custom resolver if provided
|
|
234
255
|
this.customResolver = resolver || null;
|
|
235
256
|
|
|
236
|
-
// If better-auth is
|
|
257
|
+
// If better-auth is disabled (config is null or enabled: false), return minimal module
|
|
237
258
|
// Note: We don't provide middleware classes when disabled because they depend on BetterAuthService
|
|
238
259
|
// and won't be used anyway (middleware is only applied when enabled)
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
this.logger.debug('BetterAuth is explicitly disabled - skipping initialization');
|
|
260
|
+
if (config === null || config?.enabled === false) {
|
|
261
|
+
this.logger.debug('BetterAuth is disabled - skipping initialization');
|
|
242
262
|
this.betterAuthEnabled = false;
|
|
243
263
|
return {
|
|
244
264
|
exports: [BETTER_AUTH_INSTANCE, BetterAuthService, BetterAuthUserMapper, BetterAuthRateLimiter],
|
|
@@ -285,15 +305,16 @@ export class BetterAuthModule implements NestModule, OnModuleInit {
|
|
|
285
305
|
inject: [ConfigService],
|
|
286
306
|
provide: BETTER_AUTH_INSTANCE,
|
|
287
307
|
useFactory: async (configService: ConfigService) => {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
//
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
// BetterAuth is
|
|
294
|
-
if (config?.enabled === false) {
|
|
295
|
-
this.logger.debug('BetterAuth is
|
|
308
|
+
// Get raw config (can be boolean or object)
|
|
309
|
+
const rawConfig = configService.get<boolean | IBetterAuth>('betterAuth');
|
|
310
|
+
// Normalize: true → {}, false/undefined → null
|
|
311
|
+
const config = normalizeBetterAuthConfig(rawConfig);
|
|
312
|
+
|
|
313
|
+
// BetterAuth is disabled if config is null or enabled: false
|
|
314
|
+
if (config === null || config?.enabled === false) {
|
|
315
|
+
this.logger.debug('BetterAuth is disabled');
|
|
296
316
|
this.betterAuthEnabled = false;
|
|
317
|
+
this.currentConfig = config;
|
|
297
318
|
return null;
|
|
298
319
|
}
|
|
299
320
|
|
|
@@ -315,6 +336,10 @@ export class BetterAuthModule implements NestModule, OnModuleInit {
|
|
|
315
336
|
// with fallback to jwt.secret, jwt.refresh.secret, or auto-generation
|
|
316
337
|
this.authInstance = createBetterAuthInstance({ config, db, fallbackSecrets });
|
|
317
338
|
|
|
339
|
+
// IMPORTANT: Store the config AFTER createBetterAuthInstance mutates it
|
|
340
|
+
// This ensures BetterAuthService has access to the resolved secret (with fallback applied)
|
|
341
|
+
this.currentConfig = config;
|
|
342
|
+
|
|
318
343
|
if (this.authInstance) {
|
|
319
344
|
this.logger.log('BetterAuth initialized successfully');
|
|
320
345
|
this.logEnabledFeatures(config);
|
|
@@ -323,13 +348,22 @@ export class BetterAuthModule implements NestModule, OnModuleInit {
|
|
|
323
348
|
return this.authInstance;
|
|
324
349
|
},
|
|
325
350
|
},
|
|
351
|
+
// Provide the resolved config for BetterAuthService
|
|
352
|
+
{
|
|
353
|
+
provide: BETTER_AUTH_CONFIG,
|
|
354
|
+
useFactory: () => this.currentConfig,
|
|
355
|
+
},
|
|
326
356
|
// BetterAuthService needs to be a factory that explicitly depends on BETTER_AUTH_INSTANCE
|
|
327
357
|
// to ensure proper initialization order
|
|
328
358
|
{
|
|
329
|
-
inject: [BETTER_AUTH_INSTANCE,
|
|
359
|
+
inject: [BETTER_AUTH_INSTANCE, BETTER_AUTH_CONFIG, getConnectionToken()],
|
|
330
360
|
provide: BetterAuthService,
|
|
331
|
-
useFactory: (
|
|
332
|
-
|
|
361
|
+
useFactory: (
|
|
362
|
+
authInstance: BetterAuthInstance | null,
|
|
363
|
+
resolvedConfig: IBetterAuth | null,
|
|
364
|
+
connection: Connection,
|
|
365
|
+
) => {
|
|
366
|
+
return new BetterAuthService(authInstance, connection, resolvedConfig);
|
|
333
367
|
},
|
|
334
368
|
},
|
|
335
369
|
BetterAuthUserMapper,
|
|
@@ -399,6 +433,10 @@ export class BetterAuthModule implements NestModule, OnModuleInit {
|
|
|
399
433
|
this.authInstance = createBetterAuthInstance({ config, db, fallbackSecrets });
|
|
400
434
|
}
|
|
401
435
|
|
|
436
|
+
// IMPORTANT: Store the config AFTER createBetterAuthInstance mutates it
|
|
437
|
+
// This ensures BetterAuthService has access to the resolved secret (with fallback applied)
|
|
438
|
+
this.currentConfig = config;
|
|
439
|
+
|
|
402
440
|
if (this.authInstance && !this.initLogged) {
|
|
403
441
|
this.initLogged = true;
|
|
404
442
|
this.logger.log('BetterAuth initialized');
|
|
@@ -408,13 +446,22 @@ export class BetterAuthModule implements NestModule, OnModuleInit {
|
|
|
408
446
|
return this.authInstance;
|
|
409
447
|
},
|
|
410
448
|
},
|
|
449
|
+
// Provide the resolved config for BetterAuthService
|
|
450
|
+
{
|
|
451
|
+
provide: BETTER_AUTH_CONFIG,
|
|
452
|
+
useFactory: () => this.currentConfig,
|
|
453
|
+
},
|
|
411
454
|
// BetterAuthService needs to be a factory that explicitly depends on BETTER_AUTH_INSTANCE
|
|
412
455
|
// to ensure proper initialization order
|
|
413
456
|
{
|
|
414
|
-
inject: [BETTER_AUTH_INSTANCE,
|
|
457
|
+
inject: [BETTER_AUTH_INSTANCE, BETTER_AUTH_CONFIG, getConnectionToken()],
|
|
415
458
|
provide: BetterAuthService,
|
|
416
|
-
useFactory: (
|
|
417
|
-
|
|
459
|
+
useFactory: (
|
|
460
|
+
authInstance: BetterAuthInstance | null,
|
|
461
|
+
resolvedConfig: IBetterAuth | null,
|
|
462
|
+
connection: Connection,
|
|
463
|
+
) => {
|
|
464
|
+
return new BetterAuthService(authInstance, connection, resolvedConfig);
|
|
418
465
|
},
|
|
419
466
|
},
|
|
420
467
|
BetterAuthUserMapper,
|
|
@@ -434,14 +481,23 @@ export class BetterAuthModule implements NestModule, OnModuleInit {
|
|
|
434
481
|
private static logEnabledFeatures(config: IBetterAuth): void {
|
|
435
482
|
const features: string[] = [];
|
|
436
483
|
|
|
484
|
+
// Helper to check if a plugin is enabled
|
|
485
|
+
// Supports: true, { enabled: true }, { ... } (enabled by default when config block present)
|
|
486
|
+
// Disabled only when: false or { enabled: false }
|
|
487
|
+
const isPluginEnabled = <T extends { enabled?: boolean }>(value: boolean | T | undefined): boolean => {
|
|
488
|
+
if (value === undefined || value === false) return false;
|
|
489
|
+
if (value === true) return true;
|
|
490
|
+
return value.enabled !== false;
|
|
491
|
+
};
|
|
492
|
+
|
|
437
493
|
// Plugins are enabled by default when config block is present
|
|
438
|
-
if (config.jwt
|
|
494
|
+
if (isPluginEnabled(config.jwt)) {
|
|
439
495
|
features.push('JWT');
|
|
440
496
|
}
|
|
441
|
-
if (config.twoFactor
|
|
497
|
+
if (isPluginEnabled(config.twoFactor)) {
|
|
442
498
|
features.push('2FA/TOTP');
|
|
443
499
|
}
|
|
444
|
-
if (config.passkey
|
|
500
|
+
if (isPluginEnabled(config.passkey)) {
|
|
445
501
|
features.push('Passkey/WebAuthn');
|
|
446
502
|
}
|
|
447
503
|
|