@lenne.tech/nest-server 11.10.2 → 11.10.4
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/config.env.js +16 -133
- package/dist/config.env.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +4 -0
- package/dist/core/modules/auth/core-auth.module.js +8 -4
- package/dist/core/modules/auth/core-auth.module.js.map +1 -1
- package/dist/core/modules/auth/guards/roles-guard-registry.d.ts +9 -0
- package/dist/core/modules/auth/guards/roles-guard-registry.js +30 -0
- package/dist/core/modules/auth/guards/roles-guard-registry.js.map +1 -0
- package/dist/core/modules/better-auth/better-auth.config.d.ts +3 -0
- package/dist/core/modules/better-auth/better-auth.config.js +176 -47
- package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-api.middleware.d.ts +5 -1
- package/dist/core/modules/better-auth/core-better-auth-api.middleware.js +101 -8
- package/dist/core/modules/better-auth/core-better-auth-api.middleware.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-challenge.service.d.ts +20 -0
- package/dist/core/modules/better-auth/core-better-auth-challenge.service.js +142 -0
- package/dist/core/modules/better-auth/core-better-auth-challenge.service.js.map +1 -0
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js +1 -1
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-web.helper.d.ts +2 -0
- package/dist/core/modules/better-auth/core-better-auth-web.helper.js +29 -1
- package/dist/core/modules/better-auth/core-better-auth-web.helper.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.controller.js +5 -13
- package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.middleware.d.ts +0 -1
- package/dist/core/modules/better-auth/core-better-auth.middleware.js +6 -19
- package/dist/core/modules/better-auth/core-better-auth.middleware.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.module.d.ts +5 -1
- package/dist/core/modules/better-auth/core-better-auth.module.js +74 -27
- package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.resolver.js +7 -6
- package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.service.d.ts +0 -2
- package/dist/core/modules/better-auth/core-better-auth.service.js +23 -37
- package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
- package/dist/core.module.js +10 -1
- package/dist/core.module.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/server/modules/better-auth/better-auth.module.d.ts +4 -1
- package/dist/server/modules/better-auth/better-auth.module.js +4 -1
- package/dist/server/modules/better-auth/better-auth.module.js.map +1 -1
- package/dist/server/server.module.js +1 -4
- package/dist/server/server.module.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/config.env.ts +24 -174
- package/src/core/common/interfaces/server-options.interface.ts +288 -35
- package/src/core/modules/auth/core-auth.module.ts +11 -5
- package/src/core/modules/auth/guards/roles-guard-registry.ts +57 -0
- package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +85 -56
- package/src/core/modules/better-auth/README.md +132 -35
- package/src/core/modules/better-auth/better-auth.config.ts +402 -70
- package/src/core/modules/better-auth/core-better-auth-api.middleware.ts +158 -18
- package/src/core/modules/better-auth/core-better-auth-challenge.service.ts +254 -0
- package/src/core/modules/better-auth/core-better-auth-user.mapper.ts +1 -1
- package/src/core/modules/better-auth/core-better-auth-web.helper.ts +64 -1
- package/src/core/modules/better-auth/core-better-auth.controller.ts +6 -14
- package/src/core/modules/better-auth/core-better-auth.middleware.ts +7 -20
- package/src/core/modules/better-auth/core-better-auth.module.ts +173 -38
- package/src/core/modules/better-auth/core-better-auth.resolver.ts +7 -6
- package/src/core/modules/better-auth/core-better-auth.service.ts +27 -48
- package/src/core.module.ts +21 -3
- package/src/index.ts +1 -0
- package/src/server/modules/better-auth/better-auth.module.ts +40 -10
- package/src/server/server.module.ts +2 -4
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Injectable, Logger, NestMiddleware } from '@nestjs/common';
|
|
2
2
|
import { NextFunction, Request, Response } from 'express';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { maskEmail, maskToken } from '../../common/helpers/logging.helper';
|
|
5
5
|
import { BetterAuthSessionUser, CoreBetterAuthUserMapper, MappedUser } from './core-better-auth-user.mapper';
|
|
6
6
|
import { extractSessionToken } from './core-better-auth-web.helper';
|
|
7
7
|
import { CoreBetterAuthService } from './core-better-auth.service';
|
|
@@ -33,7 +33,6 @@ export interface CoreBetterAuthRequest extends Request {
|
|
|
33
33
|
@Injectable()
|
|
34
34
|
export class CoreBetterAuthMiddleware implements NestMiddleware {
|
|
35
35
|
private readonly logger = new Logger(CoreBetterAuthMiddleware.name);
|
|
36
|
-
private readonly isProd = isProduction();
|
|
37
36
|
|
|
38
37
|
constructor(
|
|
39
38
|
private readonly betterAuthService: CoreBetterAuthService,
|
|
@@ -134,9 +133,7 @@ export class CoreBetterAuthMiddleware implements NestMiddleware {
|
|
|
134
133
|
} catch (error) {
|
|
135
134
|
// Don't block the request on auth errors
|
|
136
135
|
// The guards will handle unauthorized access
|
|
137
|
-
|
|
138
|
-
this.logger.debug(`Session validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
139
|
-
}
|
|
136
|
+
this.logger.debug(`Session validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
140
137
|
}
|
|
141
138
|
|
|
142
139
|
next();
|
|
@@ -176,26 +173,18 @@ export class CoreBetterAuthMiddleware implements NestMiddleware {
|
|
|
176
173
|
const basePath = this.betterAuthService.getBasePath();
|
|
177
174
|
const sessionToken = extractSessionToken(req, basePath);
|
|
178
175
|
|
|
179
|
-
|
|
180
|
-
this.logger.debug(`[MIDDLEWARE] getSession called, token found: ${sessionToken ? 'yes' : 'no'}`);
|
|
181
|
-
}
|
|
176
|
+
this.logger.debug(`[MIDDLEWARE] getSession called, token found: ${sessionToken ? 'yes' : 'no'}`);
|
|
182
177
|
|
|
183
178
|
if (sessionToken) {
|
|
184
|
-
|
|
185
|
-
this.logger.debug(`[MIDDLEWARE] Found session token in cookies: ${maskToken(sessionToken)}`);
|
|
186
|
-
}
|
|
179
|
+
this.logger.debug(`[MIDDLEWARE] Found session token in cookies: ${maskToken(sessionToken)}`);
|
|
187
180
|
|
|
188
181
|
// Use getSessionByToken to validate session directly from database
|
|
189
182
|
const sessionResult = await this.betterAuthService.getSessionByToken(sessionToken);
|
|
190
183
|
|
|
191
|
-
|
|
192
|
-
this.logger.debug(`[MIDDLEWARE] getSessionByToken result: user=${maskEmail(sessionResult?.user?.email)}, session=${!!sessionResult?.session}`);
|
|
193
|
-
}
|
|
184
|
+
this.logger.debug(`[MIDDLEWARE] getSessionByToken result: user=${maskEmail(sessionResult?.user?.email)}, session=${!!sessionResult?.session}`);
|
|
194
185
|
|
|
195
186
|
if (sessionResult?.user && sessionResult?.session) {
|
|
196
|
-
|
|
197
|
-
this.logger.debug(`[MIDDLEWARE] Session validated for user: ${maskEmail(sessionResult.user.email)}`);
|
|
198
|
-
}
|
|
187
|
+
this.logger.debug(`[MIDDLEWARE] Session validated for user: ${maskEmail(sessionResult.user.email)}`);
|
|
199
188
|
return sessionResult as { session: any; user: BetterAuthSessionUser };
|
|
200
189
|
}
|
|
201
190
|
}
|
|
@@ -225,9 +214,7 @@ export class CoreBetterAuthMiddleware implements NestMiddleware {
|
|
|
225
214
|
|
|
226
215
|
return null;
|
|
227
216
|
} catch (error) {
|
|
228
|
-
|
|
229
|
-
this.logger.debug(`getSession error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
230
|
-
}
|
|
217
|
+
this.logger.debug(`getSession error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
231
218
|
return null;
|
|
232
219
|
}
|
|
233
220
|
}
|
|
@@ -15,11 +15,13 @@ import mongoose, { Connection } from 'mongoose';
|
|
|
15
15
|
|
|
16
16
|
import { IBetterAuth } from '../../common/interfaces/server-options.interface';
|
|
17
17
|
import { ConfigService } from '../../common/services/config.service';
|
|
18
|
+
import { RolesGuardRegistry } from '../auth/guards/roles-guard-registry';
|
|
18
19
|
import { RolesGuard } from '../auth/guards/roles.guard';
|
|
19
20
|
import { BetterAuthTokenService } from './better-auth-token.service';
|
|
20
21
|
import { BetterAuthInstance, createBetterAuthInstance } from './better-auth.config';
|
|
21
22
|
import { DefaultBetterAuthResolver } from './better-auth.resolver';
|
|
22
23
|
import { CoreBetterAuthApiMiddleware } from './core-better-auth-api.middleware';
|
|
24
|
+
import { CoreBetterAuthChallengeService } from './core-better-auth-challenge.service';
|
|
23
25
|
import { CoreBetterAuthRateLimitMiddleware } from './core-better-auth-rate-limit.middleware';
|
|
24
26
|
import { CoreBetterAuthRateLimiter } from './core-better-auth-rate-limiter.service';
|
|
25
27
|
import { CoreBetterAuthUserMapper } from './core-better-auth-user.mapper';
|
|
@@ -38,13 +40,14 @@ export const BETTER_AUTH_INSTANCE = 'BETTER_AUTH_INSTANCE';
|
|
|
38
40
|
*/
|
|
39
41
|
export interface CoreBetterAuthModuleOptions {
|
|
40
42
|
/**
|
|
41
|
-
* Better-auth configuration.
|
|
43
|
+
* Better-auth configuration (optional - auto-read from ConfigService).
|
|
42
44
|
* Accepts:
|
|
43
45
|
* - `true`: Enable with all defaults (including JWT)
|
|
44
46
|
* - `false`: Disable BetterAuth
|
|
45
47
|
* - `{ ... }`: Enable with custom configuration
|
|
48
|
+
* - `undefined`: Auto-read from ConfigService (Zero-Config)
|
|
46
49
|
*/
|
|
47
|
-
config
|
|
50
|
+
config?: boolean | IBetterAuth;
|
|
48
51
|
|
|
49
52
|
/**
|
|
50
53
|
* Custom controller class to use instead of the default CoreBetterAuthController.
|
|
@@ -85,13 +88,14 @@ export interface CoreBetterAuthModuleOptions {
|
|
|
85
88
|
/**
|
|
86
89
|
* Register RolesGuard as a global guard.
|
|
87
90
|
*
|
|
88
|
-
* This should be set to `true` for IAM-only setups (CoreModule.forRoot with 1 parameter)
|
|
89
|
-
* where CoreAuthModule is not imported (which normally registers RolesGuard globally).
|
|
90
|
-
*
|
|
91
91
|
* When `true`, all `@Roles()` decorators will be enforced automatically without
|
|
92
92
|
* needing explicit `@UseGuards(RolesGuard)` on each endpoint.
|
|
93
93
|
*
|
|
94
|
-
*
|
|
94
|
+
* **Important:** This should be `false` in Legacy mode (3-parameter CoreModule.forRoot)
|
|
95
|
+
* because CoreAuthModule already registers RolesGuard globally. Setting it to `true`
|
|
96
|
+
* in Legacy mode would cause duplicate guard registration.
|
|
97
|
+
*
|
|
98
|
+
* @default true (secure by default - ensures @Roles() decorators are enforced)
|
|
95
99
|
*/
|
|
96
100
|
registerRolesGuardGlobally?: boolean;
|
|
97
101
|
|
|
@@ -119,19 +123,66 @@ export interface CoreBetterAuthModuleOptions {
|
|
|
119
123
|
* ```
|
|
120
124
|
*/
|
|
121
125
|
resolver?: Type<CoreBetterAuthResolver>;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Server-level app/frontend URL (from IServerOptions.appUrl).
|
|
129
|
+
* This is the frontend application URL where the browser runs.
|
|
130
|
+
*
|
|
131
|
+
* Used for:
|
|
132
|
+
* - CORS trustedOrigins
|
|
133
|
+
* - Passkey/WebAuthn origin
|
|
134
|
+
*
|
|
135
|
+
* Auto-Detection:
|
|
136
|
+
* - If not set, derived from `serverBaseUrl`:
|
|
137
|
+
* - 'https://api.example.com' → 'https://example.com'
|
|
138
|
+
* - 'https://example.com' → 'https://example.com'
|
|
139
|
+
* - When `serverEnv: 'local'` and not set: defaults to 'http://localhost:3001'
|
|
140
|
+
*
|
|
141
|
+
* @example 'https://example.com'
|
|
142
|
+
*/
|
|
143
|
+
serverAppUrl?: string;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Server-level base URL (from IServerOptions.baseUrl).
|
|
147
|
+
* This is the API server URL.
|
|
148
|
+
*
|
|
149
|
+
* Used for:
|
|
150
|
+
* - Email links (password reset, verification)
|
|
151
|
+
* - OAuth callback URLs
|
|
152
|
+
* - As fallback for betterAuth.baseUrl
|
|
153
|
+
*
|
|
154
|
+
* Auto-Detection:
|
|
155
|
+
* - When `serverEnv: 'local'` and not set: defaults to 'http://localhost:3000'
|
|
156
|
+
*
|
|
157
|
+
* @example 'https://api.example.com'
|
|
158
|
+
*/
|
|
159
|
+
serverBaseUrl?: string;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Server environment (from IServerOptions.env).
|
|
163
|
+
* Used for local environment defaults:
|
|
164
|
+
* - When `env: 'local'` and no URLs are set:
|
|
165
|
+
* - `serverBaseUrl` defaults to 'http://localhost:3000'
|
|
166
|
+
* - `serverAppUrl` defaults to 'http://localhost:3001'
|
|
167
|
+
*/
|
|
168
|
+
serverEnv?: string;
|
|
122
169
|
}
|
|
123
170
|
|
|
124
171
|
/**
|
|
125
172
|
* Normalizes betterAuth config from boolean | IBetterAuth to IBetterAuth | null
|
|
126
173
|
* - `true` → `{}` (enabled with defaults)
|
|
127
174
|
* - `false` → `null` (disabled)
|
|
128
|
-
* - `undefined` → `
|
|
175
|
+
* - `undefined` → `{}` (enabled by default - zero-config)
|
|
176
|
+
* - `{ enabled: false }` → `null` (disabled)
|
|
129
177
|
* - `{ ... }` → `{ ... }` (pass through)
|
|
130
178
|
*/
|
|
131
179
|
function normalizeBetterAuthConfig(config: boolean | IBetterAuth | undefined): IBetterAuth | null {
|
|
132
|
-
|
|
180
|
+
// BetterAuth is enabled by default (zero-config)
|
|
181
|
+
if (config === undefined || config === null) return {};
|
|
133
182
|
if (config === true) return {};
|
|
134
183
|
if (config === false) return null;
|
|
184
|
+
// Check for explicit { enabled: false }
|
|
185
|
+
if (typeof config === 'object' && config.enabled === false) return null;
|
|
135
186
|
return config;
|
|
136
187
|
}
|
|
137
188
|
|
|
@@ -169,6 +220,8 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
169
220
|
private static customController: null | Type<CoreBetterAuthController> = null;
|
|
170
221
|
private static customResolver: null | Type<CoreBetterAuthResolver> = null;
|
|
171
222
|
private static shouldRegisterRolesGuardGlobally = false;
|
|
223
|
+
// Track if registerRolesGuardGlobally was explicitly set to false (for warning)
|
|
224
|
+
private static rolesGuardExplicitlyDisabled = false;
|
|
172
225
|
|
|
173
226
|
/**
|
|
174
227
|
* Gets the controller class to use (custom or default)
|
|
@@ -199,6 +252,16 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
199
252
|
if (this.rateLimiter && CoreBetterAuthModule.currentConfig?.rateLimit) {
|
|
200
253
|
this.rateLimiter.configure(CoreBetterAuthModule.currentConfig.rateLimit);
|
|
201
254
|
}
|
|
255
|
+
|
|
256
|
+
// Security warning: Check if RolesGuard is registered when explicitly disabled
|
|
257
|
+
// This warning helps developers identify potential security misconfigurations
|
|
258
|
+
if (CoreBetterAuthModule.rolesGuardExplicitlyDisabled && !RolesGuardRegistry.isRegistered()) {
|
|
259
|
+
CoreBetterAuthModule.logger.warn(
|
|
260
|
+
'⚠️ SECURITY WARNING: registerRolesGuardGlobally is explicitly set to false, ' +
|
|
261
|
+
'but no RolesGuard is registered globally. @Roles() decorators will NOT enforce access control! ' +
|
|
262
|
+
'Either set registerRolesGuardGlobally: true, or ensure CoreAuthModule (Legacy) is imported.',
|
|
263
|
+
);
|
|
264
|
+
}
|
|
202
265
|
}
|
|
203
266
|
|
|
204
267
|
/**
|
|
@@ -268,10 +331,39 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
268
331
|
* @returns Dynamic module configuration
|
|
269
332
|
*/
|
|
270
333
|
static forRoot(options: CoreBetterAuthModuleOptions): DynamicModule {
|
|
271
|
-
const {
|
|
334
|
+
const {
|
|
335
|
+
config: rawConfig,
|
|
336
|
+
controller,
|
|
337
|
+
fallbackSecrets,
|
|
338
|
+
registerRolesGuardGlobally,
|
|
339
|
+
resolver,
|
|
340
|
+
serverAppUrl,
|
|
341
|
+
serverBaseUrl,
|
|
342
|
+
serverEnv,
|
|
343
|
+
} = options;
|
|
344
|
+
|
|
345
|
+
// Auto-read from global ConfigService if not explicitly provided
|
|
346
|
+
// This allows projects to use BetterAuthModule.forRoot({}) for true Zero-Config
|
|
347
|
+
// as all values are already available from CoreModule.forRoot(envConfig)
|
|
348
|
+
const globalConfig = ConfigService.configFastButReadOnly;
|
|
349
|
+
|
|
350
|
+
// Auto-detect config from ConfigService if not explicitly provided
|
|
351
|
+
const effectiveRawConfig = rawConfig ?? globalConfig?.betterAuth;
|
|
352
|
+
|
|
353
|
+
// Auto-detect fallbackSecrets from ConfigService if not explicitly provided
|
|
354
|
+
const effectiveFallbackSecrets = fallbackSecrets ?? (
|
|
355
|
+
globalConfig?.jwt
|
|
356
|
+
? [globalConfig.jwt.secret, globalConfig.jwt.refresh?.secret].filter(Boolean)
|
|
357
|
+
: undefined
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
// Auto-detect server URLs from ConfigService if not explicitly provided
|
|
361
|
+
const effectiveServerAppUrl = serverAppUrl ?? globalConfig?.appUrl;
|
|
362
|
+
const effectiveServerBaseUrl = serverBaseUrl ?? globalConfig?.baseUrl;
|
|
363
|
+
const effectiveServerEnv = serverEnv ?? globalConfig?.env;
|
|
272
364
|
|
|
273
365
|
// Normalize config: true → {}, false/undefined → null
|
|
274
|
-
const config = normalizeBetterAuthConfig(
|
|
366
|
+
const config = normalizeBetterAuthConfig(effectiveRawConfig);
|
|
275
367
|
|
|
276
368
|
// Store config for middleware configuration
|
|
277
369
|
this.currentConfig = config;
|
|
@@ -279,8 +371,12 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
279
371
|
this.customController = controller || null;
|
|
280
372
|
// Store custom resolver if provided
|
|
281
373
|
this.customResolver = resolver || null;
|
|
282
|
-
// Store whether to register RolesGuard globally
|
|
283
|
-
|
|
374
|
+
// Store whether to register RolesGuard globally
|
|
375
|
+
// Default is true (secure by default) - ensures @Roles() decorators are enforced
|
|
376
|
+
// CoreModule.forRoot sets this to false in Legacy mode (where CoreAuthModule handles it)
|
|
377
|
+
this.shouldRegisterRolesGuardGlobally = registerRolesGuardGlobally ?? true;
|
|
378
|
+
// Track if explicitly disabled (for security warning in onModuleInit)
|
|
379
|
+
this.rolesGuardExplicitlyDisabled = registerRolesGuardGlobally === false;
|
|
284
380
|
|
|
285
381
|
// If better-auth is disabled (config is null or enabled: false), return minimal module
|
|
286
382
|
// Note: We don't provide middleware classes when disabled because they depend on CoreBetterAuthService
|
|
@@ -289,7 +385,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
289
385
|
this.logger.debug('BetterAuth is disabled - skipping initialization');
|
|
290
386
|
this.betterAuthEnabled = false;
|
|
291
387
|
return {
|
|
292
|
-
exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter, BetterAuthTokenService],
|
|
388
|
+
exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter, BetterAuthTokenService, CoreBetterAuthChallengeService],
|
|
293
389
|
module: CoreBetterAuthModule,
|
|
294
390
|
providers: [
|
|
295
391
|
{
|
|
@@ -302,6 +398,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
302
398
|
CoreBetterAuthUserMapper,
|
|
303
399
|
CoreBetterAuthRateLimiter,
|
|
304
400
|
BetterAuthTokenService,
|
|
401
|
+
CoreBetterAuthChallengeService,
|
|
305
402
|
],
|
|
306
403
|
};
|
|
307
404
|
}
|
|
@@ -314,7 +411,12 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
314
411
|
|
|
315
412
|
// Always use deferred initialization to ensure MongoDB is ready
|
|
316
413
|
// This prevents timing issues during application startup
|
|
317
|
-
|
|
414
|
+
// Pass server-level URLs for Passkey auto-detection (using effective values from ConfigService fallback)
|
|
415
|
+
return this.createDeferredModule(config, effectiveFallbackSecrets, {
|
|
416
|
+
serverAppUrl: effectiveServerAppUrl,
|
|
417
|
+
serverBaseUrl: effectiveServerBaseUrl,
|
|
418
|
+
serverEnv: effectiveServerEnv,
|
|
419
|
+
});
|
|
318
420
|
}
|
|
319
421
|
|
|
320
422
|
/**
|
|
@@ -326,7 +428,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
326
428
|
static forRootAsync(): DynamicModule {
|
|
327
429
|
return {
|
|
328
430
|
controllers: [this.getControllerClass()],
|
|
329
|
-
exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter, BetterAuthTokenService],
|
|
431
|
+
exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter, BetterAuthTokenService, CoreBetterAuthChallengeService],
|
|
330
432
|
imports: [],
|
|
331
433
|
module: CoreBetterAuthModule,
|
|
332
434
|
providers: [
|
|
@@ -408,6 +510,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
408
510
|
return new BetterAuthTokenService(betterAuthService, connection);
|
|
409
511
|
},
|
|
410
512
|
},
|
|
513
|
+
CoreBetterAuthChallengeService,
|
|
411
514
|
this.getResolverClass(),
|
|
412
515
|
],
|
|
413
516
|
};
|
|
@@ -441,16 +544,27 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
441
544
|
this.customController = null;
|
|
442
545
|
this.customResolver = null;
|
|
443
546
|
this.shouldRegisterRolesGuardGlobally = false;
|
|
547
|
+
this.rolesGuardExplicitlyDisabled = false;
|
|
548
|
+
// Reset shared RolesGuard registry (shared with CoreAuthModule)
|
|
549
|
+
RolesGuardRegistry.reset();
|
|
444
550
|
}
|
|
445
551
|
|
|
446
552
|
/**
|
|
447
553
|
* Creates a deferred initialization module that waits for mongoose connection
|
|
448
554
|
* By injecting the Connection token, NestJS ensures Mongoose is ready first
|
|
555
|
+
*
|
|
556
|
+
* @param config - BetterAuth configuration
|
|
557
|
+
* @param fallbackSecrets - Fallback secrets for backwards compatibility
|
|
558
|
+
* @param serverUrls - Server-level URLs for Passkey auto-detection
|
|
449
559
|
*/
|
|
450
|
-
private static createDeferredModule(
|
|
560
|
+
private static createDeferredModule(
|
|
561
|
+
config: IBetterAuth,
|
|
562
|
+
fallbackSecrets?: (string | undefined)[],
|
|
563
|
+
serverUrls?: { serverAppUrl?: string; serverBaseUrl?: string; serverEnv?: string },
|
|
564
|
+
): DynamicModule {
|
|
451
565
|
return {
|
|
452
566
|
controllers: [this.getControllerClass()],
|
|
453
|
-
exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter, BetterAuthTokenService],
|
|
567
|
+
exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter, BetterAuthTokenService, CoreBetterAuthChallengeService],
|
|
454
568
|
module: CoreBetterAuthModule,
|
|
455
569
|
providers: [
|
|
456
570
|
{
|
|
@@ -467,9 +581,23 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
467
581
|
if (!globalDb) {
|
|
468
582
|
throw new Error('MongoDB database not available');
|
|
469
583
|
}
|
|
470
|
-
this.authInstance = createBetterAuthInstance({
|
|
584
|
+
this.authInstance = createBetterAuthInstance({
|
|
585
|
+
config,
|
|
586
|
+
db: globalDb,
|
|
587
|
+
fallbackSecrets,
|
|
588
|
+
serverAppUrl: serverUrls?.serverAppUrl,
|
|
589
|
+
serverBaseUrl: serverUrls?.serverBaseUrl,
|
|
590
|
+
serverEnv: serverUrls?.serverEnv,
|
|
591
|
+
});
|
|
471
592
|
} else {
|
|
472
|
-
this.authInstance = createBetterAuthInstance({
|
|
593
|
+
this.authInstance = createBetterAuthInstance({
|
|
594
|
+
config,
|
|
595
|
+
db,
|
|
596
|
+
fallbackSecrets,
|
|
597
|
+
serverAppUrl: serverUrls?.serverAppUrl,
|
|
598
|
+
serverBaseUrl: serverUrls?.serverBaseUrl,
|
|
599
|
+
serverEnv: serverUrls?.serverEnv,
|
|
600
|
+
});
|
|
473
601
|
}
|
|
474
602
|
|
|
475
603
|
// IMPORTANT: Store the config AFTER createBetterAuthInstance mutates it
|
|
@@ -516,16 +644,21 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
516
644
|
return new BetterAuthTokenService(betterAuthService, connection);
|
|
517
645
|
},
|
|
518
646
|
},
|
|
647
|
+
CoreBetterAuthChallengeService,
|
|
519
648
|
this.getResolverClass(),
|
|
520
649
|
// Conditionally register RolesGuard globally for IAM-only setups
|
|
521
650
|
// In Legacy mode, RolesGuard is already registered globally via CoreAuthModule
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
651
|
+
// Uses shared RolesGuardRegistry to prevent duplicate registration across modules
|
|
652
|
+
...(this.shouldRegisterRolesGuardGlobally && !RolesGuardRegistry.isRegistered()
|
|
653
|
+
? (() => {
|
|
654
|
+
RolesGuardRegistry.markRegistered('CoreBetterAuthModule');
|
|
655
|
+
return [
|
|
656
|
+
{
|
|
657
|
+
provide: APP_GUARD,
|
|
658
|
+
useClass: RolesGuard,
|
|
659
|
+
},
|
|
660
|
+
];
|
|
661
|
+
})()
|
|
529
662
|
: []),
|
|
530
663
|
],
|
|
531
664
|
};
|
|
@@ -539,24 +672,26 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
539
672
|
private static logEnabledFeatures(config: IBetterAuth): void {
|
|
540
673
|
const features: string[] = [];
|
|
541
674
|
|
|
542
|
-
// Helper to check if a plugin is
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
if (value === true) return true;
|
|
548
|
-
return value.enabled !== false;
|
|
675
|
+
// Helper to check if a plugin is explicitly disabled
|
|
676
|
+
const isExplicitlyDisabled = <T extends { enabled?: boolean }>(value: boolean | T | undefined): boolean => {
|
|
677
|
+
if (value === false) return true;
|
|
678
|
+
if (typeof value === 'object' && value?.enabled === false) return true;
|
|
679
|
+
return false;
|
|
549
680
|
};
|
|
550
681
|
|
|
551
|
-
//
|
|
552
|
-
if (
|
|
682
|
+
// JWT and 2FA are enabled by default unless explicitly disabled
|
|
683
|
+
if (!isExplicitlyDisabled(config.jwt)) {
|
|
553
684
|
features.push('JWT');
|
|
554
685
|
}
|
|
555
|
-
if (
|
|
686
|
+
if (!isExplicitlyDisabled(config.twoFactor)) {
|
|
556
687
|
features.push('2FA/TOTP');
|
|
557
688
|
}
|
|
558
|
-
|
|
559
|
-
|
|
689
|
+
// Passkey is enabled by default, unless explicitly set to false
|
|
690
|
+
if (config.passkey !== false && !(typeof config.passkey === 'object' && config.passkey?.enabled === false)) {
|
|
691
|
+
const passkeyConfig = typeof config.passkey === 'object' ? config.passkey : null;
|
|
692
|
+
// Challenge storage is 'database' by default, can be overridden via config
|
|
693
|
+
const challengeStorage = passkeyConfig?.challengeStorage || 'database';
|
|
694
|
+
features.push(`Passkey/WebAuthn (challenges: ${challengeStorage})`);
|
|
560
695
|
}
|
|
561
696
|
|
|
562
697
|
// Dynamically collect enabled social providers
|
|
@@ -4,6 +4,7 @@ import { Request, Response } from 'express';
|
|
|
4
4
|
|
|
5
5
|
import { Roles } from '../../common/decorators/roles.decorator';
|
|
6
6
|
import { RoleEnum } from '../../common/enums/role.enum';
|
|
7
|
+
import { maskEmail } from '../../common/helpers/logging.helper';
|
|
7
8
|
import {
|
|
8
9
|
BetterAuth2FAResponse,
|
|
9
10
|
BetterAuthSignInResponse,
|
|
@@ -228,12 +229,12 @@ export class CoreBetterAuthResolver {
|
|
|
228
229
|
body: { email, password },
|
|
229
230
|
})) as BetterAuthSignInResponse | null;
|
|
230
231
|
|
|
231
|
-
this.logger.debug(`[SignIn] API response for ${email}: ${JSON.stringify(response)?.substring(0, 200)}`);
|
|
232
|
+
this.logger.debug(`[SignIn] API response for ${maskEmail(email)}: ${JSON.stringify(response)?.substring(0, 200)}`);
|
|
232
233
|
|
|
233
234
|
// Check if response indicates an error (Better-Auth returns error objects, not throws)
|
|
234
235
|
const responseAny = response as any;
|
|
235
236
|
if (responseAny?.error || responseAny?.code === 'CREDENTIAL_ACCOUNT_NOT_FOUND') {
|
|
236
|
-
this.logger.debug(`[SignIn] API returned error for ${email}: ${responseAny?.error || responseAny?.code}`);
|
|
237
|
+
this.logger.debug(`[SignIn] API returned error for ${maskEmail(email)}: ${responseAny?.error || responseAny?.code}`);
|
|
237
238
|
throw new Error(responseAny?.error || responseAny?.code || 'Credential account not found');
|
|
238
239
|
}
|
|
239
240
|
|
|
@@ -273,17 +274,17 @@ export class CoreBetterAuthResolver {
|
|
|
273
274
|
throw new UnauthorizedException('Invalid credentials');
|
|
274
275
|
} catch (error) {
|
|
275
276
|
this.logger.debug(
|
|
276
|
-
`[SignIn] Sign-in failed for ${email}: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
277
|
+
`[SignIn] Sign-in failed for ${maskEmail(email)}: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
277
278
|
);
|
|
278
279
|
|
|
279
280
|
// If migration is allowed, try to migrate legacy user and retry
|
|
280
281
|
if (allowMigration) {
|
|
281
|
-
this.logger.debug(`[SignIn] Attempting migration for ${email}...`);
|
|
282
|
+
this.logger.debug(`[SignIn] Attempting migration for ${maskEmail(email)}...`);
|
|
282
283
|
// Pass the original password for legacy verification
|
|
283
284
|
const migrated = await this.userMapper.migrateAccountToIam(email, password);
|
|
284
|
-
this.logger.debug(`[SignIn] Migration result for ${email}: ${migrated}`);
|
|
285
|
+
this.logger.debug(`[SignIn] Migration result for ${maskEmail(email)}: ${migrated}`);
|
|
285
286
|
if (migrated) {
|
|
286
|
-
this.logger.debug(`[SignIn] Migrated legacy user ${email} to IAM, retrying sign-in`);
|
|
287
|
+
this.logger.debug(`[SignIn] Migrated legacy user ${maskEmail(email)} to IAM, retrying sign-in`);
|
|
287
288
|
// Retry sign-in after migration with normalized password (as migrateAccountToIam stores it)
|
|
288
289
|
const normalizedPassword = this.userMapper.normalizePasswordForIam(password);
|
|
289
290
|
return this.attemptSignInDirect(email, normalizedPassword, api);
|
|
@@ -4,7 +4,7 @@ import { Request } from 'express';
|
|
|
4
4
|
import { importJWK, jwtVerify } from 'jose';
|
|
5
5
|
import { Connection } from 'mongoose';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { maskCookieHeader, maskEmail, maskToken } from '../../common/helpers/logging.helper';
|
|
8
8
|
import { IBetterAuth } from '../../common/interfaces/server-options.interface';
|
|
9
9
|
import { ConfigService } from '../../common/services/config.service';
|
|
10
10
|
import { BetterAuthInstance } from './better-auth.config';
|
|
@@ -55,7 +55,6 @@ export const BETTER_AUTH_CONFIG = 'BETTER_AUTH_CONFIG';
|
|
|
55
55
|
@Injectable()
|
|
56
56
|
export class CoreBetterAuthService {
|
|
57
57
|
private readonly logger = new Logger(CoreBetterAuthService.name);
|
|
58
|
-
private readonly isProd = isProduction();
|
|
59
58
|
private readonly config: IBetterAuth;
|
|
60
59
|
|
|
61
60
|
constructor(
|
|
@@ -129,32 +128,30 @@ export class CoreBetterAuthService {
|
|
|
129
128
|
|
|
130
129
|
/**
|
|
131
130
|
* Checks if 2FA is enabled.
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* - `false`
|
|
131
|
+
* 2FA is enabled by default when BetterAuth is enabled.
|
|
132
|
+
* Only disabled when explicitly set to:
|
|
133
|
+
* - `false`
|
|
134
|
+
* - `{ enabled: false }`
|
|
135
135
|
*/
|
|
136
136
|
isTwoFactorEnabled(): boolean {
|
|
137
|
-
|
|
137
|
+
if (!this.isEnabled()) return false;
|
|
138
|
+
if (this.config.twoFactor === false) return false;
|
|
139
|
+
if (typeof this.config.twoFactor === 'object' && this.config.twoFactor?.enabled === false) return false;
|
|
140
|
+
return true;
|
|
138
141
|
}
|
|
139
142
|
|
|
140
143
|
/**
|
|
141
144
|
* Checks if Passkey/WebAuthn is enabled.
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
* - `false`
|
|
145
|
+
* Passkey is enabled by default when BetterAuth is enabled.
|
|
146
|
+
* Only disabled when explicitly set to:
|
|
147
|
+
* - `false`
|
|
148
|
+
* - `{ enabled: false }`
|
|
145
149
|
*/
|
|
146
150
|
isPasskeyEnabled(): boolean {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
* Helper to check if a plugin configuration is enabled.
|
|
152
|
-
* Supports both boolean and object configuration.
|
|
153
|
-
*/
|
|
154
|
-
private isPluginEnabled<T extends { enabled?: boolean }>(config: boolean | T | undefined): boolean {
|
|
155
|
-
if (config === undefined) return false;
|
|
156
|
-
if (typeof config === 'boolean') return config;
|
|
157
|
-
return config.enabled !== false;
|
|
151
|
+
if (!this.isEnabled()) return false;
|
|
152
|
+
if (this.config.passkey === false) return false;
|
|
153
|
+
if (typeof this.config.passkey === 'object' && this.config.passkey?.enabled === false) return false;
|
|
154
|
+
return true;
|
|
158
155
|
}
|
|
159
156
|
|
|
160
157
|
/**
|
|
@@ -320,17 +317,13 @@ export class CoreBetterAuthService {
|
|
|
320
317
|
}
|
|
321
318
|
|
|
322
319
|
// Debug: Log the cookie header being sent to api.getSession (masked for security)
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
this.logger.debug(`getSession called with cookies: ${maskCookieHeader(cookieHeader)}`);
|
|
326
|
-
}
|
|
320
|
+
const cookieHeader = headers.get('cookie');
|
|
321
|
+
this.logger.debug(`getSession called with cookies: ${maskCookieHeader(cookieHeader)}`);
|
|
327
322
|
|
|
328
323
|
const response = await api.getSession({ headers });
|
|
329
324
|
|
|
330
325
|
// Debug: Log the response from api.getSession
|
|
331
|
-
|
|
332
|
-
this.logger.debug(`getSession response: ${JSON.stringify(response)?.substring(0, 200)}`);
|
|
333
|
-
}
|
|
326
|
+
this.logger.debug(`getSession response: ${JSON.stringify(response)?.substring(0, 200)}`);
|
|
334
327
|
|
|
335
328
|
if (response && typeof response === 'object' && 'user' in response) {
|
|
336
329
|
return response as SessionResult;
|
|
@@ -338,9 +331,7 @@ export class CoreBetterAuthService {
|
|
|
338
331
|
|
|
339
332
|
return { session: null, user: null };
|
|
340
333
|
} catch (error) {
|
|
341
|
-
|
|
342
|
-
this.logger.debug(`getSession error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
343
|
-
}
|
|
334
|
+
this.logger.debug(`getSession error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
344
335
|
return { session: null, user: null };
|
|
345
336
|
}
|
|
346
337
|
}
|
|
@@ -383,9 +374,7 @@ export class CoreBetterAuthService {
|
|
|
383
374
|
await api.signOut({ headers });
|
|
384
375
|
return true;
|
|
385
376
|
} catch (error) {
|
|
386
|
-
|
|
387
|
-
this.logger.debug(`revokeSession error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
388
|
-
}
|
|
377
|
+
this.logger.debug(`revokeSession error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
389
378
|
return false;
|
|
390
379
|
}
|
|
391
380
|
}
|
|
@@ -494,31 +483,23 @@ export class CoreBetterAuthService {
|
|
|
494
483
|
const result = results[0];
|
|
495
484
|
|
|
496
485
|
if (!result) {
|
|
497
|
-
|
|
498
|
-
this.logger.debug(`getSessionByToken: session not found for token ${maskToken(token)}`);
|
|
499
|
-
}
|
|
486
|
+
this.logger.debug(`getSessionByToken: session not found for token ${maskToken(token)}`);
|
|
500
487
|
return { session: null, user: null };
|
|
501
488
|
}
|
|
502
489
|
|
|
503
490
|
// Check if session is expired
|
|
504
491
|
if (result.expiresAt && new Date(result.expiresAt) < new Date()) {
|
|
505
|
-
|
|
506
|
-
this.logger.debug(`getSessionByToken: session expired`);
|
|
507
|
-
}
|
|
492
|
+
this.logger.debug(`getSessionByToken: session expired`);
|
|
508
493
|
return { session: null, user: null };
|
|
509
494
|
}
|
|
510
495
|
|
|
511
496
|
const user = result.userDoc;
|
|
512
497
|
if (!user) {
|
|
513
|
-
|
|
514
|
-
this.logger.debug(`getSessionByToken: user not found for session`);
|
|
515
|
-
}
|
|
498
|
+
this.logger.debug(`getSessionByToken: user not found for session`);
|
|
516
499
|
return { session: null, user: null };
|
|
517
500
|
}
|
|
518
501
|
|
|
519
|
-
|
|
520
|
-
this.logger.debug(`getSessionByToken: found session for user ${maskEmail(user.email)}`);
|
|
521
|
-
}
|
|
502
|
+
this.logger.debug(`getSessionByToken: found session for user ${maskEmail(user.email)}`);
|
|
522
503
|
|
|
523
504
|
return {
|
|
524
505
|
session: {
|
|
@@ -535,9 +516,7 @@ export class CoreBetterAuthService {
|
|
|
535
516
|
},
|
|
536
517
|
};
|
|
537
518
|
} catch (error) {
|
|
538
|
-
|
|
539
|
-
this.logger.debug(`getSessionByToken error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
540
|
-
}
|
|
519
|
+
this.logger.debug(`getSessionByToken error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
541
520
|
return { session: null, user: null };
|
|
542
521
|
}
|
|
543
522
|
}
|