@lenne.tech/nest-server 11.7.1 → 11.7.3
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/guards/roles.guard.d.ts +12 -2
- package/dist/core/modules/auth/guards/roles.guard.js +192 -5
- package/dist/core/modules/auth/guards/roles.guard.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/modules/user/core-user.service.d.ts +1 -0
- package/dist/core/modules/user/core-user.service.js +12 -0
- package/dist/core/modules/user/core-user.service.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/guards/roles.guard.ts +298 -5
- 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/modules/user/core-user.service.ts +27 -0
- 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
|
@@ -10,8 +10,11 @@ Integration of the [better-auth](https://better-auth.com) authentication framewo
|
|
|
10
10
|
CoreModule.forRoot(envConfig), // IAM-only (new projects)
|
|
11
11
|
BetterAuthModule.forRoot({ config: envConfig.betterAuth, fallbackSecrets: [envConfig.jwt?.secret] }),
|
|
12
12
|
|
|
13
|
-
// 3. Configure in config.env.ts:
|
|
14
|
-
betterAuth:
|
|
13
|
+
// 3. Configure in config.env.ts (minimal - JWT enabled by default):
|
|
14
|
+
betterAuth: true // or betterAuth: {} for same effect
|
|
15
|
+
|
|
16
|
+
// With optional features:
|
|
17
|
+
betterAuth: { twoFactor: {}, passkey: {} }
|
|
15
18
|
```
|
|
16
19
|
|
|
17
20
|
**Quick Links:** [Integration Checklist](./INTEGRATION-CHECKLIST.md) | [REST API](#rest-api-endpoints) | [GraphQL API](#graphql-api) | [Configuration](#configuration)
|
|
@@ -36,11 +39,11 @@ betterAuth: { jwt: {}, twoFactor: {}, passkey: {} }
|
|
|
36
39
|
|
|
37
40
|
## Features
|
|
38
41
|
|
|
39
|
-
### Built-in Plugins
|
|
42
|
+
### Built-in Plugins
|
|
40
43
|
|
|
41
|
-
- **JWT Tokens** - For API clients and stateless authentication
|
|
42
|
-
- **Two-Factor Authentication (2FA)** - TOTP-based second factor
|
|
43
|
-
- **Passkey/WebAuthn** - Passwordless authentication
|
|
44
|
+
- **JWT Tokens** - For API clients and stateless authentication (**enabled by default**)
|
|
45
|
+
- **Two-Factor Authentication (2FA)** - TOTP-based second factor (opt-in)
|
|
46
|
+
- **Passkey/WebAuthn** - Passwordless authentication (opt-in)
|
|
44
47
|
|
|
45
48
|
### Core Features
|
|
46
49
|
|
|
@@ -93,7 +96,9 @@ https://github.com/lenneTech/nest-server/tree/develop/src/server/modules/better-
|
|
|
93
96
|
**Copy from:** Reference implementation
|
|
94
97
|
|
|
95
98
|
**WHY must ALL decorators be re-declared?**
|
|
96
|
-
GraphQL schema is built from decorators at compile time. The parent class (`CoreBetterAuthResolver`) is marked as `isAbstract: true`, so its methods are not registered in the schema. You MUST re-declare `@Query`, `@Mutation`, `@Roles
|
|
99
|
+
GraphQL schema is built from decorators at compile time. The parent class (`CoreBetterAuthResolver`) is marked as `isAbstract: true`, so its methods are not registered in the schema. You MUST re-declare `@Query`, `@Mutation`, `@Roles` decorators in the child class for the methods to appear in the GraphQL schema.
|
|
100
|
+
|
|
101
|
+
**Note:** `@UseGuards(AuthGuard(JWT))` is NOT needed when using `@Roles(S_USER)` or `@Roles(ADMIN)` because `RolesGuard` already extends `AuthGuard(JWT)` internally.
|
|
97
102
|
|
|
98
103
|
### Step 3: Create BetterAuth Controller
|
|
99
104
|
**Create:** `src/server/modules/better-auth/better-auth.controller.ts`
|
|
@@ -149,22 +154,28 @@ Add `betterAuth` configuration block. See reference for all available options in
|
|
|
149
154
|
|
|
150
155
|
## Quick Reference
|
|
151
156
|
|
|
157
|
+
**Configuration formats:**
|
|
158
|
+
```typescript
|
|
159
|
+
betterAuth: true // Enable with all defaults (JWT enabled)
|
|
160
|
+
betterAuth: false // Disable completely
|
|
161
|
+
betterAuth: {} // Same as true
|
|
162
|
+
betterAuth: { ... } // Enable with custom settings
|
|
163
|
+
betterAuth: { enabled: false } // Disable (allows pre-configuration)
|
|
164
|
+
```
|
|
165
|
+
|
|
152
166
|
**Default values (used when not configured):**
|
|
153
167
|
|
|
168
|
+
- **JWT**: Enabled by default
|
|
154
169
|
- **Secret**: Falls back to `jwt.secret` → `jwt.refresh.secret` → auto-generated
|
|
155
170
|
- **Base URL**: `http://localhost:3000`
|
|
156
171
|
- **Base Path**: `/iam`
|
|
157
|
-
- **Passkey
|
|
158
|
-
- **Passkey rpId**: `localhost`
|
|
159
|
-
- **Passkey rpName**: `Nest Server`
|
|
172
|
+
- **2FA/Passkey**: Disabled (opt-in)
|
|
160
173
|
|
|
161
174
|
To **explicitly disable** Better-Auth:
|
|
162
175
|
|
|
163
176
|
```typescript
|
|
164
177
|
const config = {
|
|
165
|
-
betterAuth: {
|
|
166
|
-
enabled: false,
|
|
167
|
-
},
|
|
178
|
+
betterAuth: false, // or betterAuth: { enabled: false }
|
|
168
179
|
};
|
|
169
180
|
```
|
|
170
181
|
|
|
@@ -330,28 +341,29 @@ const config = {
|
|
|
330
341
|
export default {
|
|
331
342
|
// ... other config
|
|
332
343
|
|
|
333
|
-
//
|
|
334
|
-
|
|
344
|
+
// MINIMAL: Just enable BetterAuth (JWT enabled by default)
|
|
345
|
+
betterAuth: true,
|
|
346
|
+
|
|
347
|
+
// OR with customization:
|
|
335
348
|
betterAuth: {
|
|
336
349
|
// enabled: true by default - only set to false to disable
|
|
337
350
|
// secret: auto-generated if not set (see Security section above)
|
|
338
351
|
// baseUrl: 'http://localhost:3000', // Default
|
|
339
352
|
// basePath: '/iam', // Default
|
|
340
353
|
|
|
341
|
-
// JWT Plugin
|
|
342
|
-
//
|
|
354
|
+
// JWT Plugin - ENABLED BY DEFAULT (no config needed)
|
|
355
|
+
// Only add this block to customize or explicitly disable
|
|
343
356
|
jwt: {
|
|
344
|
-
expiresIn: '15m'
|
|
357
|
+
expiresIn: '30m', // Default: '15m'
|
|
358
|
+
// enabled: false, // Uncomment to disable JWT
|
|
345
359
|
},
|
|
346
360
|
|
|
347
|
-
// Two-Factor Authentication (
|
|
348
|
-
// Set enabled: false to explicitly disable
|
|
361
|
+
// Two-Factor Authentication (opt-in - requires config block)
|
|
349
362
|
twoFactor: {
|
|
350
363
|
appName: 'My Application',
|
|
351
364
|
},
|
|
352
365
|
|
|
353
|
-
// Passkey/WebAuthn (
|
|
354
|
-
// Set enabled: false to explicitly disable
|
|
366
|
+
// Passkey/WebAuthn (opt-in - requires config block)
|
|
355
367
|
passkey: {
|
|
356
368
|
rpId: 'localhost',
|
|
357
369
|
rpName: 'My Application',
|
|
@@ -515,23 +527,25 @@ Better-Auth provides a rich plugin ecosystem. This module uses a **hybrid approa
|
|
|
515
527
|
- **Built-in plugins** (JWT, 2FA, Passkey): Explicitly configured with typed options
|
|
516
528
|
- **Additional plugins**: Dynamically added via the `plugins` array
|
|
517
529
|
|
|
518
|
-
### Built-in Plugins
|
|
530
|
+
### Built-in Plugins
|
|
519
531
|
|
|
520
|
-
|
|
532
|
+
| Plugin | Default State | Minimal Config to Enable | Default Values |
|
|
533
|
+
| ------------------ | ------------- | ------------------------ | --------------------------------------------------------------------------------- |
|
|
534
|
+
| **JWT** | **ENABLED** | *(none needed)* | `expiresIn: '15m'` |
|
|
535
|
+
| **Two-Factor** | Disabled | `twoFactor: {}` | `appName: 'Nest Server'` |
|
|
536
|
+
| **Passkey** | Disabled | `passkey: {}` | `origin: 'http://localhost:3000'`, `rpId: 'localhost'`, `rpName: 'Nest Server'` |
|
|
521
537
|
|
|
522
|
-
|
|
523
|
-
| ------------------ | -------------------- | --------------------------------------------------------------------------------- |
|
|
524
|
-
| **JWT** | `jwt: {}` | `expiresIn: '15m'` |
|
|
525
|
-
| **Two-Factor** | `twoFactor: {}` | `appName: 'Nest Server'` |
|
|
526
|
-
| **Passkey** | `passkey: {}` | `origin: 'http://localhost:3000'`, `rpId: 'localhost'`, `rpName: 'Nest Server'` |
|
|
538
|
+
**JWT is enabled by default** - no configuration needed. 2FA and Passkey require explicit configuration.
|
|
527
539
|
|
|
528
540
|
#### Minimal Syntax (Recommended for Development)
|
|
529
541
|
|
|
530
542
|
```typescript
|
|
531
543
|
const config = {
|
|
544
|
+
// JWT is enabled automatically with BetterAuth
|
|
545
|
+
betterAuth: true, // or betterAuth: {}
|
|
546
|
+
|
|
547
|
+
// To also enable 2FA and Passkey:
|
|
532
548
|
betterAuth: {
|
|
533
|
-
// Just add empty blocks - all defaults are applied!
|
|
534
|
-
jwt: {},
|
|
535
549
|
twoFactor: {},
|
|
536
550
|
passkey: {},
|
|
537
551
|
},
|
|
@@ -554,17 +568,20 @@ const config = {
|
|
|
554
568
|
};
|
|
555
569
|
```
|
|
556
570
|
|
|
557
|
-
#### Disabling
|
|
571
|
+
#### Disabling Plugins
|
|
558
572
|
|
|
559
573
|
```typescript
|
|
560
574
|
const config = {
|
|
561
575
|
betterAuth: {
|
|
562
|
-
jwt: { enabled: false }
|
|
563
|
-
twoFactor: {},
|
|
576
|
+
jwt: false, // Disable JWT (or jwt: { enabled: false })
|
|
577
|
+
twoFactor: {}, // 2FA enabled with defaults
|
|
578
|
+
passkey: { enabled: false }, // Passkey explicitly disabled
|
|
564
579
|
},
|
|
565
580
|
};
|
|
566
581
|
```
|
|
567
582
|
|
|
583
|
+
**Note:** JWT is the only plugin enabled by default. To disable it, use `jwt: false` or `jwt: { enabled: false }`.
|
|
584
|
+
|
|
568
585
|
### Dynamic Plugins (plugins Array)
|
|
569
586
|
|
|
570
587
|
For all other Better-Auth plugins, use the `plugins` array. This provides maximum flexibility without requiring updates to this package.
|
|
@@ -733,9 +750,9 @@ To explicitly disable Better-Auth:
|
|
|
733
750
|
|
|
734
751
|
```typescript
|
|
735
752
|
const config = {
|
|
736
|
-
betterAuth:
|
|
737
|
-
|
|
738
|
-
},
|
|
753
|
+
betterAuth: false, // Simple boolean
|
|
754
|
+
// or
|
|
755
|
+
betterAuth: { enabled: false }, // Allows pre-configuration
|
|
739
756
|
};
|
|
740
757
|
```
|
|
741
758
|
|
|
@@ -749,10 +766,30 @@ When enabled, Better-Auth exposes the following endpoints at the configured `bas
|
|
|
749
766
|
| `/iam/sign-in/email` | POST | Sign in with email/password |
|
|
750
767
|
| `/iam/sign-out` | GET | Sign out (invalidate session)|
|
|
751
768
|
| `/iam/session` | GET | Get current session |
|
|
769
|
+
| `/iam/token` | GET | Get fresh JWT token |
|
|
752
770
|
| `/iam/forgot-password` | POST | Request password reset |
|
|
753
771
|
| `/iam/reset-password` | POST | Reset password with token |
|
|
754
772
|
| `/iam/verify-email` | POST | Verify email address |
|
|
755
773
|
|
|
774
|
+
### JWT Token Endpoint
|
|
775
|
+
|
|
776
|
+
The `/iam/token` endpoint returns a fresh JWT token for the current session. Use this when your JWT has expired but your session is still valid.
|
|
777
|
+
|
|
778
|
+
**Request:**
|
|
779
|
+
```bash
|
|
780
|
+
curl -X GET https://api.example.com/iam/token \
|
|
781
|
+
-H "Cookie: better-auth.session_token=..."
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
**Response:**
|
|
785
|
+
```json
|
|
786
|
+
{
|
|
787
|
+
"token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImtpZCI6Ii4uLiJ9..."
|
|
788
|
+
}
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
**Use case:** Microservice authentication - pass the JWT to other services that verify tokens via JWKS (`/iam/jwks`) without database access.
|
|
792
|
+
|
|
756
793
|
### Social Login Endpoints
|
|
757
794
|
|
|
758
795
|
| Endpoint | Method | Description |
|
|
@@ -781,12 +818,14 @@ In addition to REST endpoints, Better-Auth provides GraphQL queries and mutation
|
|
|
781
818
|
|
|
782
819
|
### Queries
|
|
783
820
|
|
|
784
|
-
| Query | Arguments | Return Type | Description
|
|
785
|
-
| ------------------------ | --------- | ---------------------------- |
|
|
786
|
-
| `betterAuthEnabled` | - | `Boolean` | Check if Better-Auth is enabled
|
|
787
|
-
| `betterAuthFeatures` | - | `BetterAuthFeaturesModel` | Get enabled features status
|
|
788
|
-
| `betterAuthSession` | - | `BetterAuthSessionModel` | Get current session (auth req.)
|
|
789
|
-
| `
|
|
821
|
+
| Query | Arguments | Return Type | Description |
|
|
822
|
+
| ------------------------ | --------- | ---------------------------- | --------------------------------- |
|
|
823
|
+
| `betterAuthEnabled` | - | `Boolean` | Check if Better-Auth is enabled |
|
|
824
|
+
| `betterAuthFeatures` | - | `BetterAuthFeaturesModel` | Get enabled features status |
|
|
825
|
+
| `betterAuthSession` | - | `BetterAuthSessionModel` | Get current session (auth req.) |
|
|
826
|
+
| `betterAuthToken` | - | `String` | Get fresh JWT token (auth req.) |
|
|
827
|
+
| `betterAuthListPasskeys` | - | `[BetterAuthPasskeyModel]` | List user's passkeys (auth req.) |
|
|
828
|
+
| `betterAuthMigrationStatus` | - | `BetterAuthMigrationStatusModel` | Migration status (admin only) |
|
|
790
829
|
|
|
791
830
|
### Mutations
|
|
792
831
|
|
|
@@ -100,7 +100,7 @@ export class BetterAuthRateLimiter {
|
|
|
100
100
|
};
|
|
101
101
|
|
|
102
102
|
if (this.config.enabled) {
|
|
103
|
-
this.logger.
|
|
103
|
+
this.logger.debug(`Rate limiting enabled: ${this.config.max} requests per ${this.config.windowSeconds}s`);
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
|
|
@@ -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
|