@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
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import type { Response } from 'express';
|
|
2
|
+
|
|
3
|
+
import type { ICookiesConfig, ICorsConfig, IServerOptions } from '../interfaces/server-options.interface';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Module-scoped cache of `process.env.NODE_ENV === 'production'`.
|
|
7
|
+
*
|
|
8
|
+
* `process.env` reads are cheap but not free; the auth hot path calls cookie helpers
|
|
9
|
+
* on every sign-in/sign-up/refresh. Since `NODE_ENV` is established at process start
|
|
10
|
+
* and immutable for the server lifetime, we read it once at import time.
|
|
11
|
+
*
|
|
12
|
+
* @since 11.25.0
|
|
13
|
+
*/
|
|
14
|
+
const IS_PRODUCTION_NODE_ENV = process.env.NODE_ENV === 'production';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Checks whether the given environment should be treated as production-like.
|
|
18
|
+
*
|
|
19
|
+
* Production-like means auth cookies must be set with `secure: true`
|
|
20
|
+
* (HTTPS-only). Triggers when EITHER:
|
|
21
|
+
* - `env === 'production'` or `env === 'staging'` (app-level `config.env`), OR
|
|
22
|
+
* - `process.env.NODE_ENV === 'production'` (runtime Node environment)
|
|
23
|
+
*
|
|
24
|
+
* Checking both layers protects staging deployments that set `config.env = 'staging'`
|
|
25
|
+
* but do not set `NODE_ENV=production`, and vice versa.
|
|
26
|
+
*
|
|
27
|
+
* @since 11.25.0
|
|
28
|
+
*/
|
|
29
|
+
export function isProductionLikeEnv(env?: string): boolean {
|
|
30
|
+
if (env === 'production' || env === 'staging') return true;
|
|
31
|
+
return IS_PRODUCTION_NODE_ENV;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Standard cookie options for authentication cookies.
|
|
36
|
+
*
|
|
37
|
+
* Applied to both Legacy Auth (`token`, `refreshToken`) and BetterAuth cookies.
|
|
38
|
+
* Enforces baseline security (httpOnly, sameSite=lax) and environment-aware
|
|
39
|
+
* secure flag (HTTPS-only in production).
|
|
40
|
+
*
|
|
41
|
+
* @since 11.25.0
|
|
42
|
+
*/
|
|
43
|
+
export interface AuthCookieDefaultOptions {
|
|
44
|
+
httpOnly: true;
|
|
45
|
+
sameSite: 'lax';
|
|
46
|
+
secure: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Returns the default secure cookie options for authentication cookies.
|
|
51
|
+
*
|
|
52
|
+
* - `httpOnly: true` — prevents JavaScript access (XSS mitigation)
|
|
53
|
+
* - `sameSite: 'lax'` — CSRF mitigation
|
|
54
|
+
* - `secure: isProductionLikeEnv(env)` — HTTPS-only in production/staging
|
|
55
|
+
*
|
|
56
|
+
* Used by both Legacy Auth and BetterAuth cookie helpers for consistent security.
|
|
57
|
+
*
|
|
58
|
+
* @param env - App-level environment from `IServerOptions.env` (e.g. `'production'`, `'staging'`, `'ci'`).
|
|
59
|
+
* Falls back to `process.env.NODE_ENV === 'production'` if absent.
|
|
60
|
+
*
|
|
61
|
+
* @since 11.25.0
|
|
62
|
+
*/
|
|
63
|
+
export function getDefaultAuthCookieOptions(env?: string): AuthCookieDefaultOptions {
|
|
64
|
+
return {
|
|
65
|
+
httpOnly: true,
|
|
66
|
+
sameSite: 'lax',
|
|
67
|
+
secure: isProductionLikeEnv(env),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Asserts that the cookies configuration is safe for production/staging environments.
|
|
73
|
+
*
|
|
74
|
+
* Throws if `exposeTokenInBody: true` is combined with a production-like environment.
|
|
75
|
+
* Rationale: Exposing the session token in the response body negates the XSS
|
|
76
|
+
* protection of the httpOnly session cookie — it enables XSS attacks to read
|
|
77
|
+
* the token from the response body.
|
|
78
|
+
*
|
|
79
|
+
* Test environments (ci, e2e, development, local) may use `exposeTokenInBody: true`
|
|
80
|
+
* for TestHelper to read tokens — these are guarded against by the env check.
|
|
81
|
+
*
|
|
82
|
+
* @throws Error if configuration is unsafe for production/staging
|
|
83
|
+
*
|
|
84
|
+
* @since 11.25.0
|
|
85
|
+
*/
|
|
86
|
+
export function assertCookiesProductionSafe(
|
|
87
|
+
cookies: boolean | ICookiesConfig | undefined,
|
|
88
|
+
env: string | undefined,
|
|
89
|
+
): void {
|
|
90
|
+
// Use the shared production-like detection so the guard triggers symmetrically with the
|
|
91
|
+
// `secure` cookie flag: both app-level `env` ('production'/'staging') AND runtime
|
|
92
|
+
// `NODE_ENV=production` are covered. A deployment that sets only `NODE_ENV=production`
|
|
93
|
+
// but forgets the app `env` field must still be blocked from exposeTokenInBody.
|
|
94
|
+
if (isProductionLikeEnv(env) && isExposeTokenInBodyEnabled(cookies)) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
'SECURITY: cookies.exposeTokenInBody must not be true in production or staging. ' +
|
|
97
|
+
'Exposing the session token in the response body negates the XSS protection of ' +
|
|
98
|
+
'the httpOnly session cookie. If hybrid JWT+Cookie auth is required, handle the ' +
|
|
99
|
+
'token at the client layer (e.g., via getToken endpoint) instead of exposing it in ' +
|
|
100
|
+
'the login response body.',
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Sets legacy auth cookies (`token`, `refreshToken`) on the response with secure defaults.
|
|
107
|
+
*
|
|
108
|
+
* Consolidates cookie-setting logic shared between `CoreAuthController` (REST) and
|
|
109
|
+
* `CoreAuthResolver` (GraphQL). Applies standard security options (httpOnly, sameSite,
|
|
110
|
+
* secure-in-production) and honors `exposeTokenInBody` by optionally stripping tokens
|
|
111
|
+
* from the response body after setting cookies.
|
|
112
|
+
*
|
|
113
|
+
* @param res - Express Response object
|
|
114
|
+
* @param result - Auth result containing `token` and `refreshToken` (modified in place)
|
|
115
|
+
* @param cookies - The `cookies` config value from IServerOptions
|
|
116
|
+
* @param env - App-level env from `IServerOptions.env` (used for `secure` flag derivation)
|
|
117
|
+
* @returns The (possibly modified) result object
|
|
118
|
+
*
|
|
119
|
+
* @since 11.25.0
|
|
120
|
+
*/
|
|
121
|
+
export function setLegacyAuthCookies<T extends { refreshToken?: string; token?: string } | null | undefined>(
|
|
122
|
+
res: Response,
|
|
123
|
+
result: T,
|
|
124
|
+
cookies: boolean | ICookiesConfig | undefined,
|
|
125
|
+
env?: string,
|
|
126
|
+
): T {
|
|
127
|
+
if (!isCookiesEnabled(cookies)) {
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const cookieOptions = getDefaultAuthCookieOptions(env);
|
|
132
|
+
|
|
133
|
+
// If result is absent or not an object, clear any existing cookies (logout path)
|
|
134
|
+
if (!result || typeof result !== 'object') {
|
|
135
|
+
res.cookie('token', '', cookieOptions);
|
|
136
|
+
res.cookie('refreshToken', '', cookieOptions);
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
res.cookie('token', result.token || '', cookieOptions);
|
|
141
|
+
res.cookie('refreshToken', result.refreshToken || '', cookieOptions);
|
|
142
|
+
|
|
143
|
+
// Remove tokens from response body unless exposeTokenInBody is enabled
|
|
144
|
+
if (!isExposeTokenInBodyEnabled(cookies)) {
|
|
145
|
+
if (result.token) {
|
|
146
|
+
delete result.token;
|
|
147
|
+
}
|
|
148
|
+
if (result.refreshToken) {
|
|
149
|
+
delete result.refreshToken;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Checks if cookies are enabled based on the cookies config value.
|
|
158
|
+
*
|
|
159
|
+
* Follows the Boolean Shorthand Pattern:
|
|
160
|
+
* - `undefined` → true (enabled by default)
|
|
161
|
+
* - `true` → true
|
|
162
|
+
* - `false` → false
|
|
163
|
+
* - `{}` → true (presence implies enabled)
|
|
164
|
+
* - `{ enabled: true }` → true
|
|
165
|
+
* - `{ enabled: false }` → false
|
|
166
|
+
* - `{ exposeTokenInBody: true }` → true (enabled, token exposed)
|
|
167
|
+
*
|
|
168
|
+
* @since 11.25.0
|
|
169
|
+
*/
|
|
170
|
+
export function isCookiesEnabled(cookies: boolean | ICookiesConfig | undefined): boolean {
|
|
171
|
+
if (cookies === false) return false;
|
|
172
|
+
if (typeof cookies === 'object' && cookies !== null) return cookies.enabled !== false;
|
|
173
|
+
return true; // undefined or true → enabled (default)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Checks if exposeTokenInBody is enabled based on the cookies config value.
|
|
178
|
+
*
|
|
179
|
+
* When true, authentication endpoints keep the token in the response body
|
|
180
|
+
* even when cookies are active. By default false — the httpOnly cookie
|
|
181
|
+
* provides XSS protection, exposing the token in the body would negate that.
|
|
182
|
+
*
|
|
183
|
+
* @since 11.25.0
|
|
184
|
+
*/
|
|
185
|
+
export function isExposeTokenInBodyEnabled(cookies: boolean | ICookiesConfig | undefined): boolean {
|
|
186
|
+
if (typeof cookies === 'object' && cookies !== null) return cookies.exposeTokenInBody === true;
|
|
187
|
+
return false; // Default: false
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Determines whether a BetterAuth session token must be converted to a JWT
|
|
192
|
+
* before being returned to the client.
|
|
193
|
+
*
|
|
194
|
+
* Conversion is required when the client actually reads the token from the
|
|
195
|
+
* response body — otherwise the opaque session token travels via cookie only
|
|
196
|
+
* and never needs to be a JWT.
|
|
197
|
+
*
|
|
198
|
+
* Truth table:
|
|
199
|
+
*
|
|
200
|
+
* | cookies | jwtEnabled | convert? |
|
|
201
|
+
* |---------------------------------|------------|----------|
|
|
202
|
+
* | `false` (JWT-only mode) | true | yes |
|
|
203
|
+
* | `true` / `{}` (cookie-only) | true | no |
|
|
204
|
+
* | `{ exposeTokenInBody: true }` | true | yes |
|
|
205
|
+
* | any | false | no |
|
|
206
|
+
*
|
|
207
|
+
* @param cookies - The `cookies` config value from IServerOptions
|
|
208
|
+
* @param jwtEnabled - Whether the BetterAuth JWT plugin is enabled
|
|
209
|
+
* @returns true if the caller should convert the session token to a JWT
|
|
210
|
+
*
|
|
211
|
+
* @since 11.25.0
|
|
212
|
+
*/
|
|
213
|
+
export function shouldConvertSessionTokenToJwt(
|
|
214
|
+
cookies: boolean | ICookiesConfig | undefined,
|
|
215
|
+
jwtEnabled: boolean,
|
|
216
|
+
): boolean {
|
|
217
|
+
if (!jwtEnabled) return false;
|
|
218
|
+
const cookiesEnabled = isCookiesEnabled(cookies);
|
|
219
|
+
const exposeTokenInBody = isExposeTokenInBodyEnabled(cookies);
|
|
220
|
+
// Cookies-only mode: token delivered via cookie, no body JWT needed
|
|
221
|
+
if (cookiesEnabled && !exposeTokenInBody) return false;
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Checks if CORS is disabled based on the cors config value.
|
|
227
|
+
*
|
|
228
|
+
* CORS is disabled when:
|
|
229
|
+
* - `cors === false`
|
|
230
|
+
* - `cors.enabled === false`
|
|
231
|
+
*
|
|
232
|
+
* @since 11.25.0
|
|
233
|
+
*/
|
|
234
|
+
export function isCorsDisabled(cors: boolean | ICorsConfig | undefined): boolean {
|
|
235
|
+
if (cors === false) return true;
|
|
236
|
+
if (typeof cors === 'object' && cors !== null) return cors.enabled === false;
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Builds a CORS configuration object from server options.
|
|
242
|
+
*
|
|
243
|
+
* Resolution priority:
|
|
244
|
+
* 1. CORS disabled → empty object (no CORS)
|
|
245
|
+
* 2. Cookies disabled → empty object (no credentials needed, handled by simple enableCors())
|
|
246
|
+
* 3. `cors.allowAll` → `{ credentials: true, origin: true }` (mirror request origin)
|
|
247
|
+
* 4. `cors.allowedOrigins` + `appUrl`/`baseUrl` → deduplicated origin list
|
|
248
|
+
* 5. Only `appUrl`/`baseUrl` → those origins
|
|
249
|
+
* 6. Nothing configured → `{}` (no credentialed CORS — caller decides fallback)
|
|
250
|
+
*
|
|
251
|
+
* Used by both:
|
|
252
|
+
* - `CoreModule.buildCorsConfig()` for GraphQL (Apollo) CORS
|
|
253
|
+
* - `main.ts` reference implementation for REST (Express) CORS
|
|
254
|
+
*
|
|
255
|
+
* Security note: when no origins are resolvable AND cookies are enabled, the function
|
|
256
|
+
* returns `{}` rather than `{ credentials: true, origin: true }`. Returning open CORS
|
|
257
|
+
* with credentials would allow any website to make credentialed requests. Callers
|
|
258
|
+
* should either configure `appUrl`/`baseUrl`/`allowedOrigins`, enable `cors.allowAll`
|
|
259
|
+
* explicitly (for development), or accept no credentialed CORS.
|
|
260
|
+
*
|
|
261
|
+
* @param options - Server options containing `cors`, `cookies`, `appUrl`, `baseUrl`
|
|
262
|
+
* @returns CORS config object for Apollo/Express, or empty object if disabled/unconfigured
|
|
263
|
+
*
|
|
264
|
+
* @since 11.25.0
|
|
265
|
+
*/
|
|
266
|
+
export function buildCorsConfig(options: Partial<IServerOptions>): Record<string, unknown> {
|
|
267
|
+
if (isCorsDisabled(options?.cors)) {
|
|
268
|
+
return {};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (!isCookiesEnabled(options?.cookies)) {
|
|
272
|
+
return {};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const corsObj = typeof options?.cors === 'object' ? options.cors : {};
|
|
276
|
+
|
|
277
|
+
// allowAll → mirror request origin (explicit opt-in for dev/test)
|
|
278
|
+
if (corsObj.allowAll) {
|
|
279
|
+
return { credentials: true, origin: true };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Build origin list from appUrl, baseUrl, and allowedOrigins
|
|
283
|
+
const origins: string[] = [];
|
|
284
|
+
if (options?.appUrl) origins.push(options.appUrl);
|
|
285
|
+
if (options?.baseUrl) origins.push(options.baseUrl);
|
|
286
|
+
if (corsObj.allowedOrigins?.length) {
|
|
287
|
+
origins.push(...corsObj.allowedOrigins);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const uniqueOrigins = [...new Set(origins)];
|
|
291
|
+
|
|
292
|
+
if (uniqueOrigins.length > 0) {
|
|
293
|
+
return { credentials: true, origin: uniqueOrigins };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// No origins resolvable → return empty (secure default — no open CORS with credentials)
|
|
297
|
+
return {};
|
|
298
|
+
}
|
|
@@ -944,6 +944,107 @@ export interface IMultiTenancy {
|
|
|
944
944
|
cacheTtlMs?: number;
|
|
945
945
|
}
|
|
946
946
|
|
|
947
|
+
/**
|
|
948
|
+
* Cookie configuration for authentication handling.
|
|
949
|
+
*
|
|
950
|
+
* Follows the Boolean Shorthand Pattern:
|
|
951
|
+
* - `undefined` / `true`: Cookies enabled with default settings
|
|
952
|
+
* - `false`: Cookies disabled (JWT-only mode)
|
|
953
|
+
* - `{ exposeTokenInBody: true }`: Cookies enabled AND token returned in response body
|
|
954
|
+
*
|
|
955
|
+
* When cookies are enabled, the server:
|
|
956
|
+
* - Loads `cookie-parser` middleware
|
|
957
|
+
* - Sets `credentials: true` in CORS configuration
|
|
958
|
+
* - Sets signed httpOnly session cookies on authentication responses
|
|
959
|
+
*
|
|
960
|
+
* JWT authentication via `Authorization: Bearer` header works independently
|
|
961
|
+
* of the cookie configuration — it is always available regardless of this setting.
|
|
962
|
+
*
|
|
963
|
+
* @default true (cookies enabled, exposeTokenInBody disabled)
|
|
964
|
+
* @since 11.25.0
|
|
965
|
+
*/
|
|
966
|
+
export interface ICookiesConfig {
|
|
967
|
+
/**
|
|
968
|
+
* Whether cookies are enabled.
|
|
969
|
+
* @default true
|
|
970
|
+
*/
|
|
971
|
+
enabled?: boolean;
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Whether to include the session token in the response body when cookies are enabled.
|
|
975
|
+
*
|
|
976
|
+
* By default, when cookies are active, the token is removed from the response body
|
|
977
|
+
* because the httpOnly cookie protects against XSS — exposing the token in the body
|
|
978
|
+
* would negate this security benefit.
|
|
979
|
+
*
|
|
980
|
+
* Enable this only when clients need both JWT (via response body) AND cookies
|
|
981
|
+
* in parallel (e.g., hybrid mobile/web apps).
|
|
982
|
+
*
|
|
983
|
+
* @default false
|
|
984
|
+
*/
|
|
985
|
+
exposeTokenInBody?: boolean;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* CORS (Cross-Origin Resource Sharing) configuration.
|
|
990
|
+
*
|
|
991
|
+
* Controls which origins can access the API. This configuration propagates to
|
|
992
|
+
* all three CORS layers: GraphQL (Apollo), REST (Express), and BetterAuth (trustedOrigins).
|
|
993
|
+
*
|
|
994
|
+
* Follows the Boolean Shorthand Pattern:
|
|
995
|
+
* - `undefined` / `true` / `{}`: CORS enabled with auto-derived origins from `appUrl`/`baseUrl`
|
|
996
|
+
* - `false`: CORS disabled on all layers (including BetterAuth)
|
|
997
|
+
* - `{ allowedOrigins: [...] }`: Additional origins beyond `appUrl`/`baseUrl`
|
|
998
|
+
* - `{ allowAll: true }`: Allow all origins (mirrors request origin — NOT recommended for production)
|
|
999
|
+
*
|
|
1000
|
+
* When cookies are enabled (default), CORS automatically includes `credentials: true`.
|
|
1001
|
+
* Browsers reject `Access-Control-Allow-Origin: *` with credentials, so origins must
|
|
1002
|
+
* be explicitly listed or `allowAll` must be set (which mirrors the request origin).
|
|
1003
|
+
*
|
|
1004
|
+
* @default undefined (enabled, origins auto-derived from appUrl/baseUrl)
|
|
1005
|
+
* @since 11.25.0
|
|
1006
|
+
*/
|
|
1007
|
+
export interface ICorsConfig {
|
|
1008
|
+
/**
|
|
1009
|
+
* Allow all origins by mirroring the request Origin header back.
|
|
1010
|
+
*
|
|
1011
|
+
* This effectively allows any website to make credentialed requests to the API.
|
|
1012
|
+
* Convenient for development but NOT recommended for production as it enables
|
|
1013
|
+
* CSRF-like attacks from any domain.
|
|
1014
|
+
*
|
|
1015
|
+
* When true, overrides `allowedOrigins`. Also propagates to BetterAuth
|
|
1016
|
+
* (trustedOrigins is set to undefined, allowing all origins).
|
|
1017
|
+
*
|
|
1018
|
+
* @default false
|
|
1019
|
+
*/
|
|
1020
|
+
allowAll?: boolean;
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* Additional allowed origins beyond `appUrl` and `baseUrl`.
|
|
1024
|
+
*
|
|
1025
|
+
* These origins are merged with the auto-derived origins from `appUrl` and `baseUrl`
|
|
1026
|
+
* (duplicates are removed). The combined list is used for both Express/Apollo CORS
|
|
1027
|
+
* and BetterAuth's `trustedOrigins`.
|
|
1028
|
+
*
|
|
1029
|
+
* Only effective when `allowAll` is not set to `true`.
|
|
1030
|
+
*
|
|
1031
|
+
* @example ['https://admin.example.com', 'https://partner.example.com']
|
|
1032
|
+
*/
|
|
1033
|
+
allowedOrigins?: string[];
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* Whether CORS is enabled.
|
|
1037
|
+
*
|
|
1038
|
+
* When set to `false`, disables CORS on all layers:
|
|
1039
|
+
* - GraphQL: No CORS headers
|
|
1040
|
+
* - REST: `enableCors()` not called
|
|
1041
|
+
* - BetterAuth: `trustedOrigins` set to empty array (no origins allowed)
|
|
1042
|
+
*
|
|
1043
|
+
* @default true
|
|
1044
|
+
*/
|
|
1045
|
+
enabled?: boolean;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
947
1048
|
/**
|
|
948
1049
|
* Options for the server
|
|
949
1050
|
*/
|
|
@@ -1123,10 +1224,48 @@ export interface IServerOptions {
|
|
|
1123
1224
|
compression?: boolean | compression.CompressionOptions;
|
|
1124
1225
|
|
|
1125
1226
|
/**
|
|
1126
|
-
*
|
|
1227
|
+
* Cookie configuration for authentication handling.
|
|
1228
|
+
*
|
|
1229
|
+
* Controls whether session cookies are set on authentication responses and
|
|
1230
|
+
* whether `cookie-parser` middleware is loaded.
|
|
1231
|
+
*
|
|
1232
|
+
* Accepts (Boolean Shorthand Pattern):
|
|
1233
|
+
* - `undefined` / `true`: Cookies enabled (token removed from response body)
|
|
1234
|
+
* - `false`: Cookies disabled (JWT-only mode, token stays in response body)
|
|
1235
|
+
* - `{ exposeTokenInBody: true }`: Cookies enabled AND token in response body
|
|
1236
|
+
*
|
|
1237
|
+
* JWT authentication via `Authorization: Bearer` header works independently
|
|
1238
|
+
* of this setting — always available regardless of cookie configuration.
|
|
1239
|
+
*
|
|
1127
1240
|
* See: https://docs.nestjs.com/techniques/cookies
|
|
1241
|
+
*
|
|
1242
|
+
* @default true
|
|
1243
|
+
* @since 11.25.0 Changed default from false to true, added ICookiesConfig
|
|
1244
|
+
* @see ICookiesConfig
|
|
1245
|
+
*/
|
|
1246
|
+
cookies?: boolean | ICookiesConfig;
|
|
1247
|
+
|
|
1248
|
+
/**
|
|
1249
|
+
* CORS (Cross-Origin Resource Sharing) configuration.
|
|
1250
|
+
*
|
|
1251
|
+
* Controls which origins can access the API. Propagates to all layers:
|
|
1252
|
+
* GraphQL (Apollo), REST (Express), and BetterAuth (trustedOrigins).
|
|
1253
|
+
*
|
|
1254
|
+
* Origins are auto-derived from `appUrl` and `baseUrl` when available.
|
|
1255
|
+
* Use `cors.allowedOrigins` to add additional origins, or `cors.allowAll`
|
|
1256
|
+
* to allow all origins (development only).
|
|
1257
|
+
*
|
|
1258
|
+
* Accepts (Boolean Shorthand Pattern):
|
|
1259
|
+
* - `undefined` / `true` / `{}`: CORS enabled with auto-derived origins
|
|
1260
|
+
* - `false`: CORS disabled on all layers (including BetterAuth)
|
|
1261
|
+
* - `{ allowedOrigins: [...] }`: Additional origins beyond appUrl/baseUrl
|
|
1262
|
+
* - `{ allowAll: true }`: Allow all origins (not recommended for production)
|
|
1263
|
+
*
|
|
1264
|
+
* @default undefined (enabled with auto-derived origins)
|
|
1265
|
+
* @since 11.25.0
|
|
1266
|
+
* @see ICorsConfig
|
|
1128
1267
|
*/
|
|
1129
|
-
|
|
1268
|
+
cors?: boolean | ICorsConfig;
|
|
1130
1269
|
|
|
1131
1270
|
/**
|
|
1132
1271
|
* Cron jobs configuration object with the name of the cron job function as key
|
|
@@ -14,6 +14,8 @@ import { ApiCommonErrorResponses } from '../../common/decorators/common-error.de
|
|
|
14
14
|
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
|
15
15
|
import { Roles } from '../../common/decorators/roles.decorator';
|
|
16
16
|
import { RoleEnum } from '../../common/enums/role.enum';
|
|
17
|
+
import { setLegacyAuthCookies } from '../../common/helpers/cookies.helper';
|
|
18
|
+
import type { ICookiesConfig } from '../../common/interfaces/server-options.interface';
|
|
17
19
|
import { ConfigService } from '../../common/services/config.service';
|
|
18
20
|
import { AuthGuardStrategy } from './auth-guard-strategy.enum';
|
|
19
21
|
import { CoreAuthModel } from './core-auth.model';
|
|
@@ -187,30 +189,16 @@ export class CoreAuthController {
|
|
|
187
189
|
// ===================================================================================================================
|
|
188
190
|
|
|
189
191
|
/**
|
|
190
|
-
* Process cookies
|
|
192
|
+
* Process cookies — sets legacy auth cookies (token, refreshToken) with secure defaults
|
|
193
|
+
* and optionally strips tokens from the response body per `cookies.exposeTokenInBody`.
|
|
194
|
+
*
|
|
195
|
+
* Delegates to the shared `setLegacyAuthCookies()` helper to ensure consistent
|
|
196
|
+
* security options (httpOnly, sameSite=lax, secure in production) between the REST
|
|
197
|
+
* controller and GraphQL resolver.
|
|
191
198
|
*/
|
|
192
199
|
protected processCookies(res: ResponseType, result: any) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if (!result || typeof result !== 'object') {
|
|
197
|
-
res.cookie('token', '', { httpOnly: true });
|
|
198
|
-
res.cookie('refreshToken', '', { httpOnly: true });
|
|
199
|
-
return result;
|
|
200
|
-
}
|
|
201
|
-
res.cookie('token', result?.token || '', { httpOnly: true });
|
|
202
|
-
res.cookie('refreshToken', result?.refreshToken || '', { httpOnly: true });
|
|
203
|
-
|
|
204
|
-
// Remove tokens from result
|
|
205
|
-
if (result.token) {
|
|
206
|
-
delete result.token;
|
|
207
|
-
}
|
|
208
|
-
if (result.refreshToken) {
|
|
209
|
-
delete result.refreshToken;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Return prepared result
|
|
214
|
-
return result;
|
|
200
|
+
const cookiesConfig = this.configService.getFastButReadOnly<boolean | ICookiesConfig>('cookies');
|
|
201
|
+
const env = this.configService.getFastButReadOnly<string>('env');
|
|
202
|
+
return setLegacyAuthCookies(res, result, cookiesConfig, env);
|
|
215
203
|
}
|
|
216
204
|
}
|
|
@@ -6,6 +6,8 @@ import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
|
|
6
6
|
import { GraphQLServiceOptions } from '../../common/decorators/graphql-service-options.decorator';
|
|
7
7
|
import { Roles } from '../../common/decorators/roles.decorator';
|
|
8
8
|
import { RoleEnum } from '../../common/enums/role.enum';
|
|
9
|
+
import { setLegacyAuthCookies } from '../../common/helpers/cookies.helper';
|
|
10
|
+
import type { ICookiesConfig } from '../../common/interfaces/server-options.interface';
|
|
9
11
|
import { ServiceOptions } from '../../common/interfaces/service-options.interface';
|
|
10
12
|
import { ConfigService } from '../../common/services/config.service';
|
|
11
13
|
import { AuthGuardStrategy } from './auth-guard-strategy.enum';
|
|
@@ -169,30 +171,16 @@ export class CoreAuthResolver {
|
|
|
169
171
|
// ===================================================================================================================
|
|
170
172
|
|
|
171
173
|
/**
|
|
172
|
-
* Process cookies
|
|
174
|
+
* Process cookies — sets legacy auth cookies (token, refreshToken) with secure defaults
|
|
175
|
+
* and optionally strips tokens from the response body per `cookies.exposeTokenInBody`.
|
|
176
|
+
*
|
|
177
|
+
* Delegates to the shared `setLegacyAuthCookies()` helper to ensure consistent
|
|
178
|
+
* security options (httpOnly, sameSite=lax, secure in production) between the REST
|
|
179
|
+
* controller and GraphQL resolver.
|
|
173
180
|
*/
|
|
174
181
|
protected processCookies(ctx: { res: ResponseType }, result: any) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (!result || typeof result !== 'object') {
|
|
179
|
-
ctx.res.cookie('token', '', { httpOnly: true });
|
|
180
|
-
ctx.res.cookie('refreshToken', '', { httpOnly: true });
|
|
181
|
-
return result;
|
|
182
|
-
}
|
|
183
|
-
ctx.res.cookie('token', result?.token || '', { httpOnly: true });
|
|
184
|
-
ctx.res.cookie('refreshToken', result?.refreshToken || '', { httpOnly: true });
|
|
185
|
-
|
|
186
|
-
// Remove tokens from result
|
|
187
|
-
if (result.token) {
|
|
188
|
-
delete result.token;
|
|
189
|
-
}
|
|
190
|
-
if (result.refreshToken) {
|
|
191
|
-
delete result.refreshToken;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Return prepared result
|
|
196
|
-
return result;
|
|
182
|
+
const cookiesConfig = this.configService.getFastButReadOnly<boolean | ICookiesConfig>('cookies');
|
|
183
|
+
const env = this.configService.getFastButReadOnly<string>('env');
|
|
184
|
+
return setLegacyAuthCookies(ctx.res, result, cookiesConfig, env);
|
|
197
185
|
}
|
|
198
186
|
}
|
|
@@ -163,6 +163,24 @@ export class ServerModule {}
|
|
|
163
163
|
**Modify:** `src/config.env.ts`
|
|
164
164
|
**Reference:** `node_modules/@lenne.tech/nest-server/src/config.env.ts`
|
|
165
165
|
|
|
166
|
+
> **Since v11.25.0 — Cookie & CORS Defaults**
|
|
167
|
+
>
|
|
168
|
+
> Cookies are now **enabled by default** (`cookies: true`). Authentication responses
|
|
169
|
+
> set signed httpOnly session cookies and **remove the token from the response body**.
|
|
170
|
+
>
|
|
171
|
+
> For test environments where `TestHelper` reads the token from the response body
|
|
172
|
+
> (signIn → token → subsequent requests with `Authorization: Bearer`), set:
|
|
173
|
+
>
|
|
174
|
+
> ```typescript
|
|
175
|
+
> ci: { cookies: { exposeTokenInBody: true }, cors: { allowAll: true } },
|
|
176
|
+
> e2e: { cookies: { exposeTokenInBody: true }, cors: { allowAll: true } },
|
|
177
|
+
> ```
|
|
178
|
+
>
|
|
179
|
+
> **Never set `exposeTokenInBody: true` in production** — the framework throws at
|
|
180
|
+
> startup if this is detected in `production` or `staging` environments (XSS-risk guard).
|
|
181
|
+
>
|
|
182
|
+
> To keep the old behavior (cookies off, tokens in body everywhere), set `cookies: false`.
|
|
183
|
+
|
|
166
184
|
#### Zero-Config (Default):
|
|
167
185
|
|
|
168
186
|
BetterAuth is **enabled by default** with JWT + 2FA. No configuration required!
|
|
@@ -260,6 +260,13 @@ Read the security section below for production deployments.
|
|
|
260
260
|
| `basePath` | Endpoint routing | 404 on API calls |
|
|
261
261
|
| `passkey.origin` | WebAuthn security | Passkey auth fails |
|
|
262
262
|
|
|
263
|
+
**Global server-level settings that affect BetterAuth behavior (since v11.25.0):**
|
|
264
|
+
|
|
265
|
+
| Setting (top-level `IServerOptions`) | Technical Purpose | Impact of Wrong Value |
|
|
266
|
+
| ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
267
|
+
| `cookies` (`boolean \| ICookiesConfig`, default: `true`) | Controls cookie-parser middleware and session cookie setting. `cookies.exposeTokenInBody` additionally returns the token in the response body (test-only; **forbidden in production**) | Tokens missing from response body surprises test clients; `exposeTokenInBody` in prod = XSS-risk, framework throws at startup |
|
|
268
|
+
| `cors` (`boolean \| ICorsConfig`, default: enabled with auto-derived origins) | Unified CORS config — propagates to GraphQL (Apollo), REST (Express), and BetterAuth `trustedOrigins` from a single source | `cors.enabled: false` disables all three layers; `cors.allowAll` allows any origin (dev only) |
|
|
269
|
+
|
|
263
270
|
**For Development:** The defaults (`http://localhost:3000`, `/iam`) are correct.
|
|
264
271
|
|
|
265
272
|
### Passkey Auto-Detection (Recommended)
|