@lenne.tech/nest-server 11.24.4 → 11.25.1
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/.claude/rules/configurable-features.md +2 -0
- package/CLAUDE.md +13 -0
- package/FRAMEWORK-API.md +14 -2
- package/README.md +15 -0
- package/dist/config.env.js +100 -81
- package/dist/config.env.js.map +1 -1
- package/dist/core/common/helpers/cookies.helper.d.ts +19 -0
- package/dist/core/common/helpers/cookies.helper.js +109 -0
- package/dist/core/common/helpers/cookies.helper.js.map +1 -0
- package/dist/core/common/interfaces/server-options.interface.d.ts +11 -1
- package/dist/core/modules/auth/core-auth.controller.js +4 -16
- package/dist/core/modules/auth/core-auth.controller.js.map +1 -1
- package/dist/core/modules/auth/core-auth.resolver.js +4 -16
- package/dist/core/modules/auth/core-auth.resolver.js.map +1 -1
- package/dist/core/modules/auth/tokens.decorator.d.ts +1 -1
- package/dist/core/modules/better-auth/better-auth.config.d.ts +24 -1
- package/dist/core/modules/better-auth/better-auth.config.js +22 -2
- package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-api.middleware.js +3 -0
- package/dist/core/modules/better-auth/core-better-auth-api.middleware.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-cookie.helper.d.ts +3 -1
- package/dist/core/modules/better-auth/core-better-auth-cookie.helper.js +7 -3
- package/dist/core/modules/better-auth/core-better-auth-cookie.helper.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.controller.js +7 -3
- package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.module.d.ts +2 -1
- package/dist/core/modules/better-auth/core-better-auth.module.js +4 -1
- package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.service.js +5 -4
- package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
- package/dist/core/modules/migrate/templates/migration-project.template.ts +16 -2
- package/dist/core.module.d.ts +3 -1
- package/dist/core.module.js +10 -7
- 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/main.js +17 -3
- package/dist/main.js.map +1 -1
- package/dist/test/test.helper.d.ts +1 -0
- package/dist/test/test.helper.js +2 -1
- package/dist/test/test.helper.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/REQUEST-LIFECYCLE.md +78 -3
- package/migration-guides/11.24.x-to-11.25.0.md +438 -0
- package/package.json +23 -21
- package/src/config.env.ts +116 -111
- package/src/core/common/helpers/cookies.helper.ts +298 -0
- package/src/core/common/interfaces/server-options.interface.ts +141 -2
- package/src/core/modules/auth/core-auth.controller.ts +11 -23
- package/src/core/modules/auth/core-auth.resolver.ts +11 -23
- package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +18 -0
- package/src/core/modules/better-auth/README.md +7 -0
- package/src/core/modules/better-auth/better-auth.config.ts +53 -15
- package/src/core/modules/better-auth/core-better-auth-api.middleware.ts +6 -3
- package/src/core/modules/better-auth/core-better-auth-cookie.helper.ts +33 -7
- package/src/core/modules/better-auth/core-better-auth.controller.ts +12 -3
- package/src/core/modules/better-auth/core-better-auth.module.ts +16 -1
- package/src/core/modules/better-auth/core-better-auth.service.ts +26 -10
- package/src/core/modules/migrate/templates/migration-project.template.ts +16 -2
- package/src/core.module.ts +40 -12
- package/src/index.ts +1 -0
- package/src/main.ts +32 -5
- package/src/test/test.helper.ts +15 -1
|
@@ -7,7 +7,7 @@ import * as crypto from 'crypto';
|
|
|
7
7
|
import * as fs from 'fs';
|
|
8
8
|
import * as path from 'path';
|
|
9
9
|
|
|
10
|
-
import { IBetterAuth } from '../../common/interfaces/server-options.interface';
|
|
10
|
+
import { IBetterAuth, ICorsConfig } from '../../common/interfaces/server-options.interface';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Type for better-auth instance with plugins
|
|
@@ -139,6 +139,14 @@ export interface CreateBetterAuthOptions {
|
|
|
139
139
|
*/
|
|
140
140
|
serverBaseUrl?: string;
|
|
141
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Server-level CORS configuration (from IServerOptions.cors).
|
|
144
|
+
* Used to align BetterAuth's trustedOrigins with the server-level CORS config.
|
|
145
|
+
*
|
|
146
|
+
* @since 11.25.0
|
|
147
|
+
*/
|
|
148
|
+
serverCorsConfig?: boolean | ICorsConfig;
|
|
149
|
+
|
|
142
150
|
/**
|
|
143
151
|
* Server environment (from IServerOptions.env).
|
|
144
152
|
* Used for local environment defaults.
|
|
@@ -298,7 +306,11 @@ export function createBetterAuthInstance(options: CreateBetterAuthOptions): Crea
|
|
|
298
306
|
// Build configuration components (pass normalized passkey config and resolved URLs)
|
|
299
307
|
const plugins = buildPlugins(config, { passkeyNormalization, serverEnv });
|
|
300
308
|
const socialProviders = buildSocialProviders(config);
|
|
301
|
-
const trustedOrigins = buildTrustedOrigins(config, {
|
|
309
|
+
const trustedOrigins = buildTrustedOrigins(config, {
|
|
310
|
+
passkeyNormalization,
|
|
311
|
+
resolvedUrls,
|
|
312
|
+
serverCorsConfig: options.serverCorsConfig,
|
|
313
|
+
});
|
|
302
314
|
const additionalFields = buildUserFields(config);
|
|
303
315
|
|
|
304
316
|
// Resolve cross-subdomain cookies (Boolean Shorthand)
|
|
@@ -598,40 +610,66 @@ function buildSocialProviders(config: IBetterAuth): Record<string, SocialProvide
|
|
|
598
610
|
/**
|
|
599
611
|
* Builds the trusted origins array for CORS configuration.
|
|
600
612
|
*
|
|
601
|
-
* Behavior:
|
|
602
|
-
*
|
|
603
|
-
*
|
|
604
|
-
*
|
|
605
|
-
*
|
|
613
|
+
* Behavior (in priority order):
|
|
614
|
+
* 1. Explicit betterAuth.trustedOrigins → use those (always takes precedence)
|
|
615
|
+
* 2. Server CORS disabled → empty array (BetterAuth CORS also disabled)
|
|
616
|
+
* 3. Server CORS allowAll → undefined (BetterAuth allows all origins)
|
|
617
|
+
* 4. Server CORS allowedOrigins → merge with appUrl/baseUrl
|
|
618
|
+
* 5. Passkey trustedOrigins → use from normalizePasskeyConfig()
|
|
619
|
+
* 6. Fallback to resolved appUrl
|
|
620
|
+
* 7. Otherwise → undefined (allows all origins via Better-Auth's default)
|
|
606
621
|
*
|
|
607
622
|
* @param config - Better-auth configuration
|
|
608
|
-
* @param options - Passkey normalization
|
|
623
|
+
* @param options - Passkey normalization, resolved URLs, and server CORS context
|
|
609
624
|
*/
|
|
610
|
-
function buildTrustedOrigins(
|
|
625
|
+
export function buildTrustedOrigins(
|
|
611
626
|
config: IBetterAuth,
|
|
612
627
|
options: {
|
|
613
628
|
passkeyNormalization: PasskeyNormalizationResult;
|
|
614
629
|
resolvedUrls: ResolvedUrls;
|
|
630
|
+
serverCorsConfig?: boolean | ICorsConfig;
|
|
615
631
|
},
|
|
616
632
|
): string[] | undefined {
|
|
617
|
-
const { passkeyNormalization, resolvedUrls } = options;
|
|
618
|
-
|
|
633
|
+
const { passkeyNormalization, resolvedUrls, serverCorsConfig } = options;
|
|
634
|
+
|
|
635
|
+
// 1. Explicit betterAuth.trustedOrigins always takes precedence (backward compatible)
|
|
619
636
|
if (config.trustedOrigins?.length) {
|
|
620
637
|
return config.trustedOrigins;
|
|
621
638
|
}
|
|
622
639
|
|
|
623
|
-
//
|
|
640
|
+
// 2. Server CORS disabled → BetterAuth CORS also disabled
|
|
641
|
+
if (serverCorsConfig === false || (typeof serverCorsConfig === 'object' && serverCorsConfig?.enabled === false)) {
|
|
642
|
+
return [];
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// 3. Server CORS allowAll → BetterAuth allows all origins
|
|
646
|
+
if (typeof serverCorsConfig === 'object' && serverCorsConfig?.allowAll) {
|
|
647
|
+
return undefined;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// 4. Server allowedOrigins → merge with appUrl/baseUrl.
|
|
651
|
+
// Order (appUrl, baseUrl, allowedOrigins) mirrors buildCorsConfig() in cookies.helper.ts
|
|
652
|
+
// for consistency — the final Set is order-equivalent, but the array order helps readers
|
|
653
|
+
// trace which value came from which source.
|
|
654
|
+
if (typeof serverCorsConfig === 'object' && serverCorsConfig?.allowedOrigins?.length) {
|
|
655
|
+
const origins: string[] = [];
|
|
656
|
+
if (resolvedUrls.appUrl) origins.push(resolvedUrls.appUrl);
|
|
657
|
+
if (resolvedUrls.baseUrl) origins.push(resolvedUrls.baseUrl);
|
|
658
|
+
origins.push(...serverCorsConfig.allowedOrigins);
|
|
659
|
+
return [...new Set(origins)];
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// 5. If Passkey is enabled, use the trustedOrigins from normalization
|
|
624
663
|
if (passkeyNormalization.enabled && passkeyNormalization.trustedOrigins?.length) {
|
|
625
664
|
return passkeyNormalization.trustedOrigins;
|
|
626
665
|
}
|
|
627
666
|
|
|
628
|
-
// Fallback to resolved appUrl (for CORS even without Passkey)
|
|
667
|
+
// 6. Fallback to resolved appUrl (for CORS even without Passkey)
|
|
629
668
|
if (resolvedUrls.appUrl) {
|
|
630
669
|
return [resolvedUrls.appUrl];
|
|
631
670
|
}
|
|
632
671
|
|
|
633
|
-
// Otherwise, let Better-Auth handle CORS with its default behavior
|
|
634
|
-
// This allows all origins for requests without credentials
|
|
672
|
+
// 7. Otherwise, let Better-Auth handle CORS with its default behavior
|
|
635
673
|
return undefined;
|
|
636
674
|
}
|
|
637
675
|
|
|
@@ -2,6 +2,7 @@ import { Injectable, Logger, NestMiddleware, Optional } from '@nestjs/common';
|
|
|
2
2
|
import { Response as ExpressResponse, NextFunction, Request } from 'express';
|
|
3
3
|
|
|
4
4
|
import { isProduction } from '../../common/helpers/logging.helper';
|
|
5
|
+
import { ConfigService } from '../../common/services/config.service';
|
|
5
6
|
import { CoreBetterAuthChallengeService } from './core-better-auth-challenge.service';
|
|
6
7
|
import { BetterAuthCookieHelper, createCookieHelper } from './core-better-auth-cookie.helper';
|
|
7
8
|
import { extractSessionToken, sendWebResponse, signCookieValue, toWebRequest } from './core-better-auth-web.helper';
|
|
@@ -64,17 +65,19 @@ export class CoreBetterAuthApiMiddleware implements NestMiddleware {
|
|
|
64
65
|
* Gets or creates the cookie helper instance.
|
|
65
66
|
* Lazy initialization because betterAuthService may not be fully initialized in constructor.
|
|
66
67
|
*
|
|
67
|
-
* Note: Legacy cookie is not enabled in middleware
|
|
68
|
-
*
|
|
69
|
-
*
|
|
68
|
+
* Note: Legacy cookie is not enabled in middleware. The middleware only needs to set
|
|
69
|
+
* the native Better-Auth cookie. Secret is required for cookie signing (Passkey/2FA).
|
|
70
|
+
* `env` is read via the frozen ConfigService snapshot so the `secure` flag covers staging.
|
|
70
71
|
*/
|
|
71
72
|
private getCookieHelper(): BetterAuthCookieHelper {
|
|
72
73
|
if (!this.cookieHelper) {
|
|
73
74
|
const config = this.betterAuthService.getConfig();
|
|
75
|
+
const env = ConfigService.configFastButReadOnly?.env as string | undefined;
|
|
74
76
|
this.cookieHelper = createCookieHelper(
|
|
75
77
|
this.betterAuthService.getBasePath(),
|
|
76
78
|
{
|
|
77
79
|
domain: this.betterAuthService.getCookieDomain(),
|
|
80
|
+
env,
|
|
78
81
|
legacyCookieEnabled: false, // Middleware doesn't need legacy cookie
|
|
79
82
|
secret: config?.secret, // Required for cookie signing
|
|
80
83
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Logger } from '@nestjs/common';
|
|
2
2
|
import { Response } from 'express';
|
|
3
3
|
|
|
4
|
+
import { isProductionLikeEnv } from '../../common/helpers/cookies.helper';
|
|
4
5
|
import { signCookieValue } from './core-better-auth-web.helper';
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -45,6 +46,14 @@ export interface BetterAuthCookieHelperConfig {
|
|
|
45
46
|
* @example 'example.com' → cookies shared across api.example.com, ws.example.com, etc.
|
|
46
47
|
*/
|
|
47
48
|
domain?: string;
|
|
49
|
+
/**
|
|
50
|
+
* App-level environment from `IServerOptions.env` (e.g. `'production'`, `'staging'`).
|
|
51
|
+
* Used to derive the `secure` flag for cookies in conjunction with `process.env.NODE_ENV`.
|
|
52
|
+
*
|
|
53
|
+
* @since 11.25.0 Covers staging deployments where `config.env === 'staging'` but
|
|
54
|
+
* `NODE_ENV !== 'production'`.
|
|
55
|
+
*/
|
|
56
|
+
env?: string;
|
|
48
57
|
/**
|
|
49
58
|
* Enable legacy 'token' cookie for backwards compatibility with < 11.7.0.
|
|
50
59
|
* Only needed when Legacy Auth (Passport) is also active.
|
|
@@ -122,13 +131,17 @@ export class BetterAuthCookieHelper {
|
|
|
122
131
|
/**
|
|
123
132
|
* Gets the default cookie options for authentication cookies.
|
|
124
133
|
*
|
|
134
|
+
* The `secure` flag is `true` when either `config.env` is production-like
|
|
135
|
+
* (`production` / `staging`) or `NODE_ENV === 'production'`. This covers
|
|
136
|
+
* staging deployments where only one of the two signals is set.
|
|
137
|
+
*
|
|
125
138
|
* @returns Cookie options with httpOnly, sameSite, and secure settings
|
|
126
139
|
*/
|
|
127
140
|
getDefaultCookieOptions(): AuthCookieOptions {
|
|
128
141
|
const options: AuthCookieOptions = {
|
|
129
142
|
httpOnly: true,
|
|
130
143
|
sameSite: 'lax',
|
|
131
|
-
secure:
|
|
144
|
+
secure: isProductionLikeEnv(this.config.env),
|
|
132
145
|
};
|
|
133
146
|
|
|
134
147
|
if (this.config.domain) {
|
|
@@ -286,18 +299,28 @@ export class BetterAuthCookieHelper {
|
|
|
286
299
|
}
|
|
287
300
|
|
|
288
301
|
/**
|
|
289
|
-
* Processes a result object by setting appropriate cookies and
|
|
302
|
+
* Processes a result object by setting appropriate cookies and optionally keeping token in body.
|
|
290
303
|
*
|
|
291
304
|
* This method handles the common pattern of:
|
|
292
305
|
* 1. Setting cookies if cookie handling is enabled
|
|
293
|
-
* 2. Removing the token from the response body (
|
|
306
|
+
* 2. Removing the token from the response body (unless exposeTokenInBody is true)
|
|
307
|
+
*
|
|
308
|
+
* When exposeTokenInBody is false (default), the token is removed from the response
|
|
309
|
+
* body because the httpOnly cookie provides XSS protection — exposing the token in
|
|
310
|
+
* the body would negate this security benefit.
|
|
294
311
|
*
|
|
295
312
|
* @param res - Express Response object
|
|
296
313
|
* @param result - The result object to process (modified in place)
|
|
297
314
|
* @param cookiesEnabled - Whether cookie handling is enabled (from config)
|
|
315
|
+
* @param exposeTokenInBody - Whether to keep the token in the response body (default: false)
|
|
298
316
|
* @returns The modified result (same reference as input)
|
|
299
317
|
*/
|
|
300
|
-
processAuthResult<T extends CookieProcessingResult>(
|
|
318
|
+
processAuthResult<T extends CookieProcessingResult>(
|
|
319
|
+
res: Response,
|
|
320
|
+
result: T,
|
|
321
|
+
cookiesEnabled: boolean,
|
|
322
|
+
exposeTokenInBody: boolean = false,
|
|
323
|
+
): T {
|
|
301
324
|
if (!cookiesEnabled) {
|
|
302
325
|
return result;
|
|
303
326
|
}
|
|
@@ -306,8 +329,10 @@ export class BetterAuthCookieHelper {
|
|
|
306
329
|
// Set cookies
|
|
307
330
|
this.setSessionCookies(res, result.token);
|
|
308
331
|
|
|
309
|
-
// Remove token from response body
|
|
310
|
-
|
|
332
|
+
// Remove token from response body unless exposeTokenInBody is enabled
|
|
333
|
+
if (!exposeTokenInBody) {
|
|
334
|
+
delete result.token;
|
|
335
|
+
}
|
|
311
336
|
}
|
|
312
337
|
|
|
313
338
|
return result;
|
|
@@ -326,12 +351,13 @@ export class BetterAuthCookieHelper {
|
|
|
326
351
|
*/
|
|
327
352
|
export function createCookieHelper(
|
|
328
353
|
basePath: string,
|
|
329
|
-
options?: { domain?: string; legacyCookieEnabled?: boolean; secret?: string },
|
|
354
|
+
options?: { domain?: string; env?: string; legacyCookieEnabled?: boolean; secret?: string },
|
|
330
355
|
logger?: Logger,
|
|
331
356
|
): BetterAuthCookieHelper {
|
|
332
357
|
return new BetterAuthCookieHelper({
|
|
333
358
|
basePath,
|
|
334
359
|
domain: options?.domain,
|
|
360
|
+
env: options?.env,
|
|
335
361
|
legacyCookieEnabled: options?.legacyCookieEnabled ?? false,
|
|
336
362
|
logger,
|
|
337
363
|
secret: options?.secret,
|
|
@@ -27,6 +27,8 @@ import { Request, Response } from 'express';
|
|
|
27
27
|
|
|
28
28
|
import { Roles } from '../../common/decorators/roles.decorator';
|
|
29
29
|
import { RoleEnum } from '../../common/enums/role.enum';
|
|
30
|
+
import { isCookiesEnabled, isExposeTokenInBodyEnabled } from '../../common/helpers/cookies.helper';
|
|
31
|
+
import type { ICookiesConfig } from '../../common/interfaces/server-options.interface';
|
|
30
32
|
import { maskEmail, maskToken } from '../../common/helpers/logging.helper';
|
|
31
33
|
import { ConfigService } from '../../common/services/config.service';
|
|
32
34
|
import { ErrorCode } from '../error-code/error-codes';
|
|
@@ -224,6 +226,7 @@ export class CoreBetterAuthController {
|
|
|
224
226
|
this.betterAuthService.getBasePath(),
|
|
225
227
|
{
|
|
226
228
|
domain: this.betterAuthService.getCookieDomain(),
|
|
229
|
+
env: this.configService.getFastButReadOnly<string>('env'),
|
|
227
230
|
legacyCookieEnabled: legacyAuthEnabled,
|
|
228
231
|
secret: betterAuthConfig?.secret,
|
|
229
232
|
},
|
|
@@ -828,19 +831,25 @@ export class CoreBetterAuthController {
|
|
|
828
831
|
result: CoreBetterAuthResponse,
|
|
829
832
|
sessionToken?: string,
|
|
830
833
|
): CoreBetterAuthResponse {
|
|
831
|
-
|
|
834
|
+
// Cookies config is read per-request (not cached in the constructor) because tests
|
|
835
|
+
// and runtime config changes (e.g. `ConfigService.setProperty('cookies', ...)`) must
|
|
836
|
+
// take effect immediately. The `getFastButReadOnly` call is a single property access
|
|
837
|
+
// on a frozen snapshot — overhead is negligible.
|
|
838
|
+
const cookiesConfig = this.configService.getFastButReadOnly<boolean | ICookiesConfig>('cookies');
|
|
839
|
+
const cookiesEnabled = isCookiesEnabled(cookiesConfig);
|
|
840
|
+
const exposeTokenInBody = isExposeTokenInBodyEnabled(cookiesConfig);
|
|
832
841
|
|
|
833
842
|
// If a specific session token is provided, use it directly
|
|
834
843
|
if (sessionToken && cookiesEnabled) {
|
|
835
844
|
this.cookieHelper.setSessionCookies(res, sessionToken, result.session?.id);
|
|
836
|
-
if (result.token) {
|
|
845
|
+
if (!exposeTokenInBody && result.token) {
|
|
837
846
|
delete result.token;
|
|
838
847
|
}
|
|
839
848
|
return result;
|
|
840
849
|
}
|
|
841
850
|
|
|
842
851
|
// Otherwise, use the cookie helper's standard processing
|
|
843
|
-
return this.cookieHelper.processAuthResult(res, result, cookiesEnabled);
|
|
852
|
+
return this.cookieHelper.processAuthResult(res, result, cookiesEnabled, exposeTokenInBody);
|
|
844
853
|
}
|
|
845
854
|
|
|
846
855
|
/**
|
|
@@ -13,7 +13,7 @@ import { APP_GUARD } from '@nestjs/core';
|
|
|
13
13
|
import { getConnectionToken } from '@nestjs/mongoose';
|
|
14
14
|
import mongoose, { Connection } from 'mongoose';
|
|
15
15
|
|
|
16
|
-
import { IBetterAuth } from '../../common/interfaces/server-options.interface';
|
|
16
|
+
import { IBetterAuth, ICorsConfig } from '../../common/interfaces/server-options.interface';
|
|
17
17
|
import { BrevoService } from '../../common/services/brevo.service';
|
|
18
18
|
import { ConfigService } from '../../common/services/config.service';
|
|
19
19
|
import { RolesGuardRegistry } from '../auth/guards/roles-guard-registry';
|
|
@@ -161,6 +161,14 @@ export interface CoreBetterAuthModuleOptions {
|
|
|
161
161
|
*/
|
|
162
162
|
serverBaseUrl?: string;
|
|
163
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Server-level CORS configuration (from IServerOptions.cors).
|
|
166
|
+
* Used to align BetterAuth's trustedOrigins with the server-level CORS config.
|
|
167
|
+
*
|
|
168
|
+
* @since 11.25.0
|
|
169
|
+
*/
|
|
170
|
+
serverCorsConfig?: boolean | ICorsConfig;
|
|
171
|
+
|
|
164
172
|
/**
|
|
165
173
|
* Server environment (from IServerOptions.env).
|
|
166
174
|
* Used for local environment defaults:
|
|
@@ -452,6 +460,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
452
460
|
resolver,
|
|
453
461
|
serverAppUrl,
|
|
454
462
|
serverBaseUrl,
|
|
463
|
+
serverCorsConfig,
|
|
455
464
|
serverEnv,
|
|
456
465
|
} = options;
|
|
457
466
|
|
|
@@ -535,10 +544,14 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
535
544
|
// Always use deferred initialization to ensure MongoDB is ready
|
|
536
545
|
// This prevents timing issues during application startup
|
|
537
546
|
// Pass server-level URLs for Passkey auto-detection (using effective values from ConfigService fallback)
|
|
547
|
+
// Auto-detect server CORS config from ConfigService if not explicitly provided
|
|
548
|
+
const effectiveServerCorsConfig = serverCorsConfig ?? globalConfig?.cors;
|
|
549
|
+
|
|
538
550
|
this.cachedDynamicModule = this.createDeferredModule(config, {
|
|
539
551
|
fallbackSecrets: effectiveFallbackSecrets,
|
|
540
552
|
serverAppUrl: effectiveServerAppUrl,
|
|
541
553
|
serverBaseUrl: effectiveServerBaseUrl,
|
|
554
|
+
serverCorsConfig: effectiveServerCorsConfig,
|
|
542
555
|
serverEnv: effectiveServerEnv,
|
|
543
556
|
});
|
|
544
557
|
return this.cachedDynamicModule;
|
|
@@ -816,6 +829,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
816
829
|
fallbackSecrets?: (string | undefined)[];
|
|
817
830
|
serverAppUrl?: string;
|
|
818
831
|
serverBaseUrl?: string;
|
|
832
|
+
serverCorsConfig?: boolean | ICorsConfig;
|
|
819
833
|
serverEnv?: string;
|
|
820
834
|
},
|
|
821
835
|
): DynamicModule {
|
|
@@ -872,6 +886,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
872
886
|
sendVerificationEmail,
|
|
873
887
|
serverAppUrl: options?.serverAppUrl,
|
|
874
888
|
serverBaseUrl: options?.serverBaseUrl,
|
|
889
|
+
serverCorsConfig: options?.serverCorsConfig,
|
|
875
890
|
serverEnv: options?.serverEnv,
|
|
876
891
|
};
|
|
877
892
|
|
|
@@ -4,8 +4,9 @@ import { Request } from 'express';
|
|
|
4
4
|
import { importJWK, jwtVerify } from 'jose';
|
|
5
5
|
import { Connection } from 'mongoose';
|
|
6
6
|
|
|
7
|
+
import { shouldConvertSessionTokenToJwt } from '../../common/helpers/cookies.helper';
|
|
7
8
|
import { maskEmail, maskToken } from '../../common/helpers/logging.helper';
|
|
8
|
-
import { IBetterAuth } from '../../common/interfaces/server-options.interface';
|
|
9
|
+
import { IBetterAuth, ICookiesConfig } from '../../common/interfaces/server-options.interface';
|
|
9
10
|
import { ConfigService } from '../../common/services/config.service';
|
|
10
11
|
import { ErrorCode } from '../error-code/error-codes';
|
|
11
12
|
import { BetterAuthInstance } from './better-auth.config';
|
|
@@ -288,20 +289,32 @@ export class CoreBetterAuthService implements OnModuleInit {
|
|
|
288
289
|
// ===================================================================================================================
|
|
289
290
|
|
|
290
291
|
/**
|
|
291
|
-
* Resolves a session token to a JWT when
|
|
292
|
+
* Resolves a session token to a JWT when JWT conversion is needed.
|
|
292
293
|
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
294
|
+
* JWT conversion is performed when:
|
|
295
|
+
* - Cookies are disabled (JWT-only mode) — clients need a JWT in the response body
|
|
296
|
+
* - exposeTokenInBody is true — clients need a JWT alongside cookies for parallel auth
|
|
297
|
+
*
|
|
298
|
+
* When cookies are enabled WITHOUT exposeTokenInBody, the token stays as a session token
|
|
299
|
+
* (it's delivered via cookie, not the response body).
|
|
300
|
+
*
|
|
301
|
+
* On conversion failure (BetterAuth JWT plugin not warmed up, transient error, etc.),
|
|
302
|
+
* this method returns `undefined` rather than the raw session token. Returning the raw
|
|
303
|
+
* session token in a body meant for a JWT would expose a database-backed opaque identifier
|
|
304
|
+
* outside the httpOnly cookie boundary (see SEC-005, v11.25.0). Callers must handle
|
|
305
|
+
* `undefined` by forcing client retry / re-authentication.
|
|
296
306
|
*
|
|
297
307
|
* @param token - The token from BetterAuth response (may be session token or JWT)
|
|
298
|
-
* @returns A proper JWT
|
|
308
|
+
* @returns A proper JWT, the original token if conversion is not needed, or `undefined`
|
|
309
|
+
* when conversion was needed but failed.
|
|
299
310
|
*/
|
|
300
311
|
async resolveJwtToken(token: string | undefined): Promise<string | undefined> {
|
|
301
312
|
if (!token) return undefined;
|
|
302
313
|
|
|
303
|
-
const
|
|
304
|
-
|
|
314
|
+
const cookiesConfig = ConfigService.configFastButReadOnly?.cookies as boolean | ICookiesConfig | undefined;
|
|
315
|
+
|
|
316
|
+
// Skip conversion when cookies-only mode or JWT plugin disabled
|
|
317
|
+
if (!shouldConvertSessionTokenToJwt(cookiesConfig, this.isJwtEnabled())) {
|
|
305
318
|
return token;
|
|
306
319
|
}
|
|
307
320
|
|
|
@@ -325,8 +338,11 @@ export class CoreBetterAuthService implements OnModuleInit {
|
|
|
325
338
|
return jwt;
|
|
326
339
|
}
|
|
327
340
|
|
|
328
|
-
|
|
329
|
-
|
|
341
|
+
// Do NOT return the raw session token as fallback — that would leak an opaque
|
|
342
|
+
// DB identifier into the response body where clients expect a JWT. Force the
|
|
343
|
+
// caller to either retry or surface an auth error to the client.
|
|
344
|
+
this.logger.warn('Failed to resolve session token to JWT - returning undefined to avoid session token exposure');
|
|
345
|
+
return undefined;
|
|
330
346
|
}
|
|
331
347
|
|
|
332
348
|
/**
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { getDb, uploadFileToGridFS } from '@lenne.tech/nest-server';
|
|
2
2
|
import { Db, ObjectId } from 'mongodb';
|
|
3
|
-
import config from '../src/config.env';
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* Migration template for nest-server projects
|
|
@@ -11,9 +10,24 @@ import config from '../src/config.env';
|
|
|
11
10
|
* Available helpers:
|
|
12
11
|
* - getDb(uri): Get MongoDB connection
|
|
13
12
|
* - uploadFileToGridFS(uri, filePath, options): Upload file to GridFS
|
|
13
|
+
*
|
|
14
|
+
* MongoDB URI resolution (in order):
|
|
15
|
+
* 1. config.env.ts (local development via ts-node)
|
|
16
|
+
* 2. NSC__MONGOOSE__URI environment variable (Docker production)
|
|
14
17
|
*/
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
let MONGO_URL: string;
|
|
20
|
+
try {
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
22
|
+
const config = require('../src/config.env');
|
|
23
|
+
MONGO_URL = (config.default || config).mongoose.uri;
|
|
24
|
+
} catch {
|
|
25
|
+
// Fallback for Docker production where config.env.ts is not available as TypeScript source
|
|
26
|
+
if (!process.env.NSC__MONGOOSE__URI) {
|
|
27
|
+
throw new Error('MongoDB URI not available. Set NSC__MONGOOSE__URI or ensure config.env.ts is loadable.');
|
|
28
|
+
}
|
|
29
|
+
MONGO_URL = process.env.NSC__MONGOOSE__URI;
|
|
30
|
+
}
|
|
17
31
|
|
|
18
32
|
/**
|
|
19
33
|
* Run migration
|
package/src/core.module.ts
CHANGED
|
@@ -12,7 +12,18 @@ import { CheckResponseInterceptor } from './core/common/interceptors/check-respo
|
|
|
12
12
|
import { CheckSecurityInterceptor } from './core/common/interceptors/check-security.interceptor';
|
|
13
13
|
import { ResponseModelInterceptor } from './core/common/interceptors/response-model.interceptor';
|
|
14
14
|
import { TranslateResponseInterceptor } from './core/common/interceptors/translate-response.interceptor';
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
assertCookiesProductionSafe,
|
|
17
|
+
buildCorsConfig,
|
|
18
|
+
isCookiesEnabled,
|
|
19
|
+
isCorsDisabled,
|
|
20
|
+
isExposeTokenInBodyEnabled,
|
|
21
|
+
} from './core/common/helpers/cookies.helper';
|
|
22
|
+
import {
|
|
23
|
+
ICookiesConfig,
|
|
24
|
+
ICoreModuleOverrides,
|
|
25
|
+
IServerOptions,
|
|
26
|
+
} from './core/common/interfaces/server-options.interface';
|
|
16
27
|
import { RequestContextMiddleware } from './core/common/middleware/request-context.middleware';
|
|
17
28
|
import { MapAndValidatePipe } from './core/common/pipes/map-and-validate.pipe';
|
|
18
29
|
import { ComplexityPlugin } from './core/common/plugins/complexity.plugin';
|
|
@@ -169,14 +180,15 @@ export class CoreModule implements NestModule {
|
|
|
169
180
|
? (authModuleOrUndefined as ICoreModuleOverrides | undefined)
|
|
170
181
|
: overridesOrUndefined;
|
|
171
182
|
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
183
|
+
// Guard against unsafe cookie configuration in production/staging.
|
|
184
|
+
// Throws if `cookies.exposeTokenInBody: true` is set in a production-like environment.
|
|
185
|
+
assertCookiesProductionSafe(options?.cookies, options?.env);
|
|
186
|
+
|
|
187
|
+
// Process CORS config (unified across GraphQL, REST, and BetterAuth).
|
|
188
|
+
// When CORS is explicitly disabled, pass `false` to Apollo — not `{}`.
|
|
189
|
+
// Apollo treats `cors: {}` as "open CORS with no credentials" (via Express cors() defaults),
|
|
190
|
+
// so we must distinguish the "disabled" case from the "no origins configured" case.
|
|
191
|
+
const cors: false | object = isCorsDisabled(options?.cors) ? false : buildCorsConfig(options);
|
|
180
192
|
|
|
181
193
|
// Determine if GraphQL is enabled (false means explicitly disabled)
|
|
182
194
|
const isGraphQlEnabled = options.graphQl !== false;
|
|
@@ -421,6 +433,8 @@ export class CoreModule implements NestModule {
|
|
|
421
433
|
// When env: 'local', defaults are: baseUrl=localhost:3000, appUrl=localhost:3001
|
|
422
434
|
serverAppUrl: config.appUrl,
|
|
423
435
|
serverBaseUrl: config.baseUrl,
|
|
436
|
+
// Pass server-level CORS config so BetterAuth trustedOrigins aligns
|
|
437
|
+
serverCorsConfig: config.cors,
|
|
424
438
|
serverEnv: config.env,
|
|
425
439
|
}),
|
|
426
440
|
);
|
|
@@ -472,7 +486,7 @@ export class CoreModule implements NestModule {
|
|
|
472
486
|
* Uses CoreBetterAuthService for subscription authentication via JWT tokens.
|
|
473
487
|
* This is the recommended mode for new projects.
|
|
474
488
|
*/
|
|
475
|
-
private static buildIamOnlyGraphQlDriver(cors: object, options: Partial<IServerOptions>) {
|
|
489
|
+
private static buildIamOnlyGraphQlDriver(cors: false | object, options: Partial<IServerOptions>) {
|
|
476
490
|
// This method is only called when graphQl !== false, extract config with type narrowing
|
|
477
491
|
const graphQlOpts = typeof options?.graphQl === 'object' ? options.graphQl : undefined;
|
|
478
492
|
return {
|
|
@@ -569,7 +583,7 @@ export class CoreModule implements NestModule {
|
|
|
569
583
|
* This is safe because `onConnect` is only called when a WebSocket connection is made,
|
|
570
584
|
* which happens after all modules are initialized.
|
|
571
585
|
*/
|
|
572
|
-
private static buildLazyIamGraphQlDriver(cors: object, options: Partial<IServerOptions>) {
|
|
586
|
+
private static buildLazyIamGraphQlDriver(cors: false | object, options: Partial<IServerOptions>) {
|
|
573
587
|
// This method is only called when graphQl !== false, extract config with type narrowing
|
|
574
588
|
const graphQlOpts = typeof options?.graphQl === 'object' ? options.graphQl : undefined;
|
|
575
589
|
return {
|
|
@@ -675,7 +689,7 @@ export class CoreModule implements NestModule {
|
|
|
675
689
|
private static buildLegacyGraphQlDriver(
|
|
676
690
|
AuthService: any,
|
|
677
691
|
AuthModule: any,
|
|
678
|
-
cors: object,
|
|
692
|
+
cors: false | object,
|
|
679
693
|
options: Partial<IServerOptions>,
|
|
680
694
|
) {
|
|
681
695
|
// This method is only called when graphQl !== false, extract config with type narrowing
|
|
@@ -746,4 +760,18 @@ export class CoreModule implements NestModule {
|
|
|
746
760
|
),
|
|
747
761
|
};
|
|
748
762
|
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* @deprecated Use `isCookiesEnabled` from `core/common/helpers/cookies.helper` instead.
|
|
766
|
+
*/
|
|
767
|
+
static isCookiesEnabled(cookies: boolean | ICookiesConfig | undefined): boolean {
|
|
768
|
+
return isCookiesEnabled(cookies);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* @deprecated Use `isExposeTokenInBodyEnabled` from `core/common/helpers/cookies.helper` instead.
|
|
773
|
+
*/
|
|
774
|
+
static isExposeTokenInBodyEnabled(cookies: boolean | ICookiesConfig | undefined): boolean {
|
|
775
|
+
return isExposeTokenInBodyEnabled(cookies);
|
|
776
|
+
}
|
|
749
777
|
}
|
package/src/index.ts
CHANGED
|
@@ -30,6 +30,7 @@ export * from './core/common/filters/http-exception-log.filter';
|
|
|
30
30
|
export * from './core/common/helpers/common.helper';
|
|
31
31
|
export * from './core/common/helpers/config.helper';
|
|
32
32
|
export * from './core/common/helpers/context.helper';
|
|
33
|
+
export * from './core/common/helpers/cookies.helper';
|
|
33
34
|
export * from './core/common/helpers/db.helper';
|
|
34
35
|
export * from './core/common/helpers/decorator.helper';
|
|
35
36
|
export * from './core/common/helpers/file.helper';
|
package/src/main.ts
CHANGED
|
@@ -7,6 +7,7 @@ import cookieParser = require('cookie-parser');
|
|
|
7
7
|
|
|
8
8
|
import envConfig from './config.env';
|
|
9
9
|
import { FilterArgs } from './core/common/args/filter.args';
|
|
10
|
+
import { buildCorsConfig, isCookiesEnabled, isCorsDisabled } from './core/common/helpers/cookies.helper';
|
|
10
11
|
import { HttpExceptionLogFilter } from './core/common/filters/http-exception-log.filter';
|
|
11
12
|
import { CorePersistenceModel } from './core/common/models/core-persistence.model';
|
|
12
13
|
import { CoreAuthModel } from './core/modules/auth/core-auth.model';
|
|
@@ -47,9 +48,18 @@ async function bootstrap() {
|
|
|
47
48
|
server.use(compression(compressionOptions));
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
// Cookie handling
|
|
51
|
-
if
|
|
52
|
-
|
|
51
|
+
// Cookie handling (enabled by default, disable with cookies: false)
|
|
52
|
+
// Pass a signing secret (if configured) to cookieParser so signed cookies can be
|
|
53
|
+
// verified via req.signedCookies. BetterAuth signs its own session cookies
|
|
54
|
+
// independently via HMAC, but the Express layer still benefits from a secret —
|
|
55
|
+
// Legacy Auth cookies and any custom signed cookies rely on this.
|
|
56
|
+
//
|
|
57
|
+
// Fallback chain: jwt.secret → betterAuth.secret (IAM-only mode) → unsigned
|
|
58
|
+
const cookiesEnabled = isCookiesEnabled(envConfig.cookies);
|
|
59
|
+
if (cookiesEnabled) {
|
|
60
|
+
const betterAuthSecret = typeof envConfig.betterAuth === 'object' ? envConfig.betterAuth?.secret : undefined;
|
|
61
|
+
const cookieSecret = envConfig.jwt?.secret || betterAuthSecret;
|
|
62
|
+
server.use(cookieSecret ? cookieParser(cookieSecret) : cookieParser());
|
|
53
63
|
}
|
|
54
64
|
|
|
55
65
|
// Asset directory
|
|
@@ -59,8 +69,25 @@ async function bootstrap() {
|
|
|
59
69
|
server.setBaseViewsDir(envConfig.templates.path);
|
|
60
70
|
server.setViewEngine(envConfig.templates.engine);
|
|
61
71
|
|
|
62
|
-
// Enable
|
|
63
|
-
|
|
72
|
+
// Enable CORS (unified with GraphQL and BetterAuth via shared buildCorsConfig helper).
|
|
73
|
+
//
|
|
74
|
+
// Three cases:
|
|
75
|
+
// 1. CORS explicitly disabled (`cors: false` or `cors.enabled: false`) → skip `enableCors()`
|
|
76
|
+
// entirely. No CORS headers emitted. Same-origin requests still work.
|
|
77
|
+
// 2. Origins resolvable (appUrl/baseUrl/allowedOrigins or allowAll) → use the computed options.
|
|
78
|
+
// 3. Cookies disabled → permissive `enableCors()` without credentials (backward-compatible).
|
|
79
|
+
if (isCorsDisabled(envConfig.cors)) {
|
|
80
|
+
// Case 1 — CORS explicitly disabled. Do nothing.
|
|
81
|
+
} else {
|
|
82
|
+
const corsOptions = buildCorsConfig(envConfig);
|
|
83
|
+
if (Object.keys(corsOptions).length > 0) {
|
|
84
|
+
server.enableCors(corsOptions); // Case 2
|
|
85
|
+
} else if (!cookiesEnabled) {
|
|
86
|
+
server.enableCors(); // Case 3
|
|
87
|
+
}
|
|
88
|
+
// else: cookies enabled but no origins resolvable → secure default (no `enableCors()` call
|
|
89
|
+
// to avoid open CORS with credentials). Callers should configure appUrl/baseUrl/allowedOrigins.
|
|
90
|
+
}
|
|
64
91
|
|
|
65
92
|
// Swagger documentation
|
|
66
93
|
const config = new DocumentBuilder()
|
package/src/test/test.helper.ts
CHANGED
|
@@ -78,6 +78,19 @@ export interface TestGraphQLOptions {
|
|
|
78
78
|
*/
|
|
79
79
|
convertEnums?: boolean | string[];
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Cookies for the request. Three usage modes (same as `TestRestOptions.cookies`):
|
|
83
|
+
* - string (plain token without = or ;): Auto-detected as session token
|
|
84
|
+
* and converted via `buildBetterAuthCookies()` to all relevant cookie names
|
|
85
|
+
* (iam.session_token, token)
|
|
86
|
+
* - string (with = or ;): Used as-is as a cookie string
|
|
87
|
+
* - Record<string, string>: Key-value pairs joined into a cookie string
|
|
88
|
+
*
|
|
89
|
+
* Use this for cookie-based authentication (default since 11.25.0). Can be
|
|
90
|
+
* combined with `token` — both headers will be sent.
|
|
91
|
+
*/
|
|
92
|
+
cookies?: Record<string, string> | string;
|
|
93
|
+
|
|
81
94
|
/**
|
|
82
95
|
* Count of subscription messages, specifies how many messages are to be received on subscription
|
|
83
96
|
*/
|
|
@@ -305,7 +318,7 @@ export class TestHelper {
|
|
|
305
318
|
};
|
|
306
319
|
|
|
307
320
|
// Init vars
|
|
308
|
-
const { language, log, logError, statusCode, token, variables } = config;
|
|
321
|
+
const { cookies, language, log, logError, statusCode, token, variables } = config;
|
|
309
322
|
|
|
310
323
|
// Init
|
|
311
324
|
let query = '';
|
|
@@ -402,6 +415,7 @@ export class TestHelper {
|
|
|
402
415
|
|
|
403
416
|
// Request configuration
|
|
404
417
|
const requestConfig: any = {
|
|
418
|
+
cookies,
|
|
405
419
|
method: 'POST',
|
|
406
420
|
payload: { query },
|
|
407
421
|
url: '/graphql',
|