@lenne.tech/nest-server 11.11.0 → 11.12.0

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.
Files changed (45) hide show
  1. package/dist/core/modules/auth/core-auth.controller.js +1 -1
  2. package/dist/core/modules/auth/core-auth.controller.js.map +1 -1
  3. package/dist/core/modules/auth/core-auth.resolver.js +1 -1
  4. package/dist/core/modules/auth/core-auth.resolver.js.map +1 -1
  5. package/dist/core/modules/better-auth/better-auth-token.service.js +1 -4
  6. package/dist/core/modules/better-auth/better-auth-token.service.js.map +1 -1
  7. package/dist/core/modules/better-auth/better-auth.config.js +55 -8
  8. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
  9. package/dist/core/modules/better-auth/core-better-auth-api.middleware.d.ts +2 -0
  10. package/dist/core/modules/better-auth/core-better-auth-api.middleware.js +22 -18
  11. package/dist/core/modules/better-auth/core-better-auth-api.middleware.js.map +1 -1
  12. package/dist/core/modules/better-auth/core-better-auth-cookie.helper.d.ts +41 -0
  13. package/dist/core/modules/better-auth/core-better-auth-cookie.helper.js +107 -0
  14. package/dist/core/modules/better-auth/core-better-auth-cookie.helper.js.map +1 -0
  15. package/dist/core/modules/better-auth/core-better-auth-token.helper.d.ts +16 -0
  16. package/dist/core/modules/better-auth/core-better-auth-token.helper.js +66 -0
  17. package/dist/core/modules/better-auth/core-better-auth-token.helper.js.map +1 -0
  18. package/dist/core/modules/better-auth/core-better-auth-web.helper.d.ts +2 -3
  19. package/dist/core/modules/better-auth/core-better-auth-web.helper.js +33 -22
  20. package/dist/core/modules/better-auth/core-better-auth-web.helper.js.map +1 -1
  21. package/dist/core/modules/better-auth/core-better-auth.controller.d.ts +2 -0
  22. package/dist/core/modules/better-auth/core-better-auth.controller.js +17 -34
  23. package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
  24. package/dist/core/modules/better-auth/core-better-auth.middleware.d.ts +0 -1
  25. package/dist/core/modules/better-auth/core-better-auth.middleware.js +2 -20
  26. package/dist/core/modules/better-auth/core-better-auth.middleware.js.map +1 -1
  27. package/dist/core/modules/better-auth/core-better-auth.service.js +22 -2
  28. package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
  29. package/dist/core/modules/better-auth/index.d.ts +2 -0
  30. package/dist/core/modules/better-auth/index.js +2 -0
  31. package/dist/core/modules/better-auth/index.js.map +1 -1
  32. package/dist/tsconfig.build.tsbuildinfo +1 -1
  33. package/package.json +10 -10
  34. package/src/core/modules/auth/core-auth.controller.ts +2 -2
  35. package/src/core/modules/auth/core-auth.resolver.ts +2 -2
  36. package/src/core/modules/better-auth/better-auth-token.service.ts +5 -8
  37. package/src/core/modules/better-auth/better-auth.config.ts +139 -12
  38. package/src/core/modules/better-auth/core-better-auth-api.middleware.ts +44 -20
  39. package/src/core/modules/better-auth/core-better-auth-cookie.helper.ts +323 -0
  40. package/src/core/modules/better-auth/core-better-auth-token.helper.ts +200 -0
  41. package/src/core/modules/better-auth/core-better-auth-web.helper.ts +73 -36
  42. package/src/core/modules/better-auth/core-better-auth.controller.ts +43 -69
  43. package/src/core/modules/better-auth/core-better-auth.middleware.ts +4 -33
  44. package/src/core/modules/better-auth/core-better-auth.service.ts +31 -9
  45. package/src/core/modules/better-auth/index.ts +2 -0
@@ -2,12 +2,18 @@ import { Logger } from '@nestjs/common';
2
2
  import * as crypto from 'crypto';
3
3
  import { Request, Response } from 'express';
4
4
 
5
+ import { isSessionToken } from './core-better-auth-token.helper';
6
+
5
7
  /**
6
8
  * Cookie names used by Better Auth and nest-server
9
+ *
10
+ * ## Cookie Strategy (v11.12+)
11
+ *
12
+ * Only the minimum required cookies are used:
13
+ * - `{basePath}.session_token` (e.g., `iam.session_token`) - Better-Auth native (ALWAYS)
14
+ * - `token` - Legacy compatibility (only if Legacy Auth active)
7
15
  */
8
16
  export const BETTER_AUTH_COOKIE_NAMES = {
9
- /** Better Auth's default session token cookie */
10
- BETTER_AUTH_SESSION: 'better-auth.session_token',
11
17
  /** Legacy nest-server token cookie */
12
18
  TOKEN: 'token',
13
19
  } as const;
@@ -37,30 +43,35 @@ export interface ToWebRequestOptions {
37
43
  /**
38
44
  * Extracts the session token from Express request cookies or Authorization header.
39
45
  *
40
- * Checks multiple cookie names for compatibility with different configurations:
41
- * 1. `{basePath}.session_token` - Based on configured basePath (e.g., iam.session_token)
42
- * 2. `better-auth.session_token` - Better Auth default
46
+ * Cookie priority (v11.12+):
47
+ * 1. Authorization: Bearer header (if session token, not JWT)
48
+ * 2. `{basePath}.session_token` (e.g., `iam.session_token`) - Better-Auth native
43
49
  * 3. `token` - Legacy nest-server cookie
44
- * 4. Authorization: Bearer header
45
50
  *
46
51
  * @param req - Express request
47
52
  * @param basePath - Base path for cookie names (e.g., '/iam' or 'iam')
48
53
  * @returns Session token or null if not found
49
54
  */
50
55
  export function extractSessionToken(req: Request, basePath: string = 'iam'): null | string {
51
- // Check Authorization header first
56
+ // Check Authorization header for session tokens (but NOT JWTs).
57
+ // JWTs (3 dot-separated parts) are handled separately by the session middleware.
58
+ // Returning a JWT here would cause toWebRequest() to overwrite valid session
59
+ // cookies with the JWT, breaking Better Auth's session lookup.
52
60
  const authHeader = req.headers.authorization;
53
61
  if (authHeader?.startsWith('Bearer ')) {
54
- return authHeader.substring(7);
62
+ const bearerToken = authHeader.substring(7);
63
+ if (isSessionToken(bearerToken)) {
64
+ return bearerToken;
65
+ }
55
66
  }
56
67
 
57
68
  // Normalize basePath (remove leading slash, replace slashes with dots)
58
69
  const normalizedBasePath = basePath.replace(/^\//, '').replace(/\//g, '.');
59
70
 
60
71
  // Cookie names to check (in order of priority)
72
+ // v11.12+: Only native Better-Auth cookie and legacy token
61
73
  const cookieNames = [
62
- `${normalizedBasePath}.session_token`, // Based on configured basePath
63
- BETTER_AUTH_COOKIE_NAMES.BETTER_AUTH_SESSION, // Better Auth default
74
+ `${normalizedBasePath}.session_token`, // Better-Auth native (PRIMARY)
64
75
  BETTER_AUTH_COOKIE_NAMES.TOKEN, // Legacy nest-server cookie
65
76
  ];
66
77
 
@@ -70,6 +81,11 @@ export function extractSessionToken(req: Request, basePath: string = 'iam'): nul
70
81
  for (const name of cookieNames) {
71
82
  const token = cookies?.[name];
72
83
  if (token && typeof token === 'string') {
84
+ // If the cookie value is signed (TOKEN.SIGNATURE format), extract the raw token.
85
+ // Better Auth signs session cookies; the DB stores only the raw token.
86
+ if (isAlreadySigned(token)) {
87
+ return token.split('.')[0];
88
+ }
73
89
  return token;
74
90
  }
75
91
  }
@@ -137,7 +153,14 @@ export function parseCookieHeader(cookieHeader: string | undefined): Record<stri
137
153
  for (const pair of pairs) {
138
154
  const [name, ...valueParts] = pair.trim().split('=');
139
155
  if (name && valueParts.length > 0) {
140
- cookies[name.trim()] = valueParts.join('=').trim();
156
+ const rawValue = valueParts.join('=').trim();
157
+ // URL-decode cookie values (standard behavior matching cookie-parser/npm cookie package)
158
+ // Express's res.cookie() URL-encodes values, so we must decode them when parsing
159
+ try {
160
+ cookies[name.trim()] = decodeURIComponent(rawValue);
161
+ } catch {
162
+ cookies[name.trim()] = rawValue;
163
+ }
141
164
  }
142
165
  }
143
166
 
@@ -157,11 +180,23 @@ export async function sendWebResponse(res: Response, webResponse: globalThis.Res
157
180
  // Set status code
158
181
  res.status(webResponse.status);
159
182
 
160
- // Copy headers
183
+ // Handle Set-Cookie headers separately
184
+ // Headers.forEach() either combines Set-Cookie values (invalid for browsers)
185
+ // or overwrites previous values with setHeader(). Use getSetCookie() instead.
186
+ // IMPORTANT: Merge with any existing Set-Cookie headers (e.g., from res.cookie() calls)
187
+ // to avoid overwriting compatibility cookies set before sendWebResponse is called.
188
+ const setCookieHeaders = webResponse.headers.getSetCookie?.() || [];
189
+ if (setCookieHeaders.length > 0) {
190
+ const existing = res.getHeader('set-cookie');
191
+ const existingHeaders = existing ? (Array.isArray(existing) ? existing.map(String) : [String(existing)]) : [];
192
+ res.setHeader('set-cookie', [...existingHeaders, ...setCookieHeaders]);
193
+ }
194
+
195
+ // Copy other headers
161
196
  webResponse.headers.forEach((value, key) => {
162
197
  // Skip certain headers that Express handles differently
163
198
  const lowerKey = key.toLowerCase();
164
- if (lowerKey === 'content-encoding' || lowerKey === 'transfer-encoding') {
199
+ if (lowerKey === 'set-cookie' || lowerKey === 'content-encoding' || lowerKey === 'transfer-encoding') {
165
200
  return;
166
201
  }
167
202
  res.setHeader(key, value);
@@ -192,20 +227,20 @@ export async function sendWebResponse(res: Response, webResponse: globalThis.Res
192
227
  *
193
228
  * @param value - The raw cookie value to sign
194
229
  * @param secret - The secret to use for signing
195
- * @returns The signed cookie value (URL-encoded)
230
+ * @param urlEncode - Whether to URL-encode the result (default: false)
231
+ * Set to true when building cookie header strings manually.
232
+ * Set to false when using Express res.cookie() which encodes automatically.
233
+ * @returns The signed cookie value
196
234
  * @throws Error if secret is not provided
197
235
  */
198
- export function signCookieValue(value: string, secret: string): string {
236
+ export function signCookieValue(value: string, secret: string, urlEncode = false): string {
199
237
  if (!secret) {
200
238
  throw new Error('Cannot sign cookie: Better Auth secret is not configured');
201
239
  }
202
240
 
203
- const signature = crypto
204
- .createHmac('sha256', secret)
205
- .update(value)
206
- .digest('base64');
241
+ const signature = crypto.createHmac('sha256', secret).update(value).digest('base64');
207
242
  const signedValue = `${value}.${signature}`;
208
- return encodeURIComponent(signedValue);
243
+ return urlEncode ? encodeURIComponent(signedValue) : signedValue;
209
244
  }
210
245
 
211
246
  /**
@@ -215,16 +250,22 @@ export function signCookieValue(value: string, secret: string): string {
215
250
  *
216
251
  * @param value - The cookie value to potentially sign
217
252
  * @param secret - The secret to use for signing
253
+ * @param urlEncode - Whether to URL-encode the result (default: true for backwards compatibility)
254
+ * Set to true when building cookie header strings manually.
255
+ * Set to false when using Express res.cookie() which encodes automatically.
218
256
  * @param logger - Optional logger for debug output
219
- * @returns The signed cookie value (URL-encoded) or the original if already signed
257
+ * @returns The signed cookie value or the original if already signed
220
258
  */
221
- export function signCookieValueIfNeeded(value: string, secret: string, logger?: Logger): string {
259
+ export function signCookieValueIfNeeded(value: string, secret: string, urlEncode = true, logger?: Logger): string {
222
260
  if (isAlreadySigned(value)) {
223
261
  logger?.debug?.('Cookie value appears to be already signed, skipping signing');
224
- // Return URL-encoded to match signCookieValue behavior
225
- return value.includes('%') ? value : encodeURIComponent(value);
262
+ // Return URL-encoded if requested and not already encoded
263
+ if (urlEncode) {
264
+ return value.includes('%') ? value : encodeURIComponent(value);
265
+ }
266
+ return value.includes('%') ? decodeURIComponent(value) : value;
226
267
  }
227
- return signCookieValue(value, secret);
268
+ return signCookieValue(value, secret, urlEncode);
228
269
  }
229
270
 
230
271
  /**
@@ -264,30 +305,26 @@ export async function toWebRequest(req: Request, options: ToWebRequestOptions):
264
305
 
265
306
  // Sign the session token for Better Auth (if secret is provided)
266
307
  // IMPORTANT: Only sign if not already signed to prevent double-signing
308
+ // URL-encode because we're building the cookie header string manually
267
309
  let signedToken: string;
268
310
  if (secret) {
269
- signedToken = signCookieValueIfNeeded(sessionToken, secret, logger);
311
+ signedToken = signCookieValueIfNeeded(sessionToken, secret, true, logger);
270
312
  } else {
271
313
  logger?.warn('No Better Auth secret configured - cookies will not be signed');
272
314
  signedToken = sessionToken;
273
315
  }
274
316
 
275
- // Cookie names that need signed tokens
317
+ // Primary cookie name for Better-Auth (e.g., 'iam.session_token')
276
318
  const primaryCookieName = `${normalizedBasePath}.session_token`;
277
- const sessionCookieNames = [
278
- primaryCookieName,
279
- BETTER_AUTH_COOKIE_NAMES.BETTER_AUTH_SESSION,
280
- ];
281
319
 
282
320
  // Parse existing cookies
283
321
  const existingCookies = parseCookieHeader(existingCookieString);
284
322
 
285
- // Replace session token cookies with signed versions
286
- for (const cookieName of sessionCookieNames) {
287
- existingCookies[cookieName] = signedToken;
288
- }
323
+ // Set the signed session token on the primary cookie
324
+ // This is the ONLY cookie Better-Auth needs
325
+ existingCookies[primaryCookieName] = signedToken;
289
326
 
290
- // Keep the unsigned token cookie for nest-server compatibility
327
+ // Keep the unsigned token cookie for legacy nest-server compatibility
291
328
  if (!existingCookies[BETTER_AUTH_COOKIE_NAMES.TOKEN]) {
292
329
  existingCookies[BETTER_AUTH_COOKIE_NAMES.TOKEN] = sessionToken;
293
330
  }
@@ -22,6 +22,7 @@ import { maskEmail, maskToken } from '../../common/helpers/logging.helper';
22
22
  import { ConfigService } from '../../common/services/config.service';
23
23
  import { ErrorCode } from '../error-code/error-codes';
24
24
  import { BetterAuthSignInResponse, hasSession, hasUser, requires2FA } from './better-auth.types';
25
+ import { BetterAuthCookieHelper, createCookieHelper } from './core-better-auth-cookie.helper';
25
26
  import { BetterAuthSessionUser, CoreBetterAuthUserMapper } from './core-better-auth-user.mapper';
26
27
  import { sendWebResponse, toWebRequest } from './core-better-auth-web.helper';
27
28
  import { CoreBetterAuthService } from './core-better-auth.service';
@@ -182,12 +183,32 @@ export class CoreBetterAuthSignUpInput {
182
183
  @Roles(RoleEnum.ADMIN)
183
184
  export class CoreBetterAuthController {
184
185
  protected readonly logger = new Logger(CoreBetterAuthController.name);
186
+ protected readonly cookieHelper: BetterAuthCookieHelper;
185
187
 
186
188
  constructor(
187
189
  protected readonly betterAuthService: CoreBetterAuthService,
188
190
  protected readonly userMapper: CoreBetterAuthUserMapper,
189
191
  protected readonly configService: ConfigService,
190
- ) {}
192
+ ) {
193
+ // Detect if Legacy Auth is active (for < 11.7.0 compatibility)
194
+ // Legacy Auth is active when JWT secret is configured
195
+ const jwtConfig = this.configService.getFastButReadOnly('jwt');
196
+ const legacyAuthEnabled = !!(jwtConfig?.secret || jwtConfig?.secretOrPrivateKey);
197
+
198
+ // Get Better-Auth secret for cookie signing
199
+ // CRITICAL: Cookies must be signed for Passkey/2FA to work
200
+ const betterAuthConfig = this.betterAuthService.getConfig();
201
+
202
+ // Initialize cookie helper with Legacy Auth detection and secret
203
+ this.cookieHelper = createCookieHelper(
204
+ this.betterAuthService.getBasePath(),
205
+ {
206
+ legacyCookieEnabled: legacyAuthEnabled,
207
+ secret: betterAuthConfig?.secret,
208
+ },
209
+ this.logger,
210
+ );
211
+ }
191
212
 
192
213
  // ===================================================================================================================
193
214
  // Authentication Endpoints
@@ -435,7 +456,7 @@ export class CoreBetterAuthController {
435
456
  * Sign out (logout)
436
457
  *
437
458
  * **Why Custom Implementation (not hooks):**
438
- * - Must clear multiple cookies (token, session, better-auth.session_token, etc.)
459
+ * - Must clear session cookies (basePath.session_token + optional legacy token)
439
460
  * - Hooks cannot modify response or set/clear cookies
440
461
  *
441
462
  * NOTE: Better-Auth uses POST for sign-out (matches better-auth convention)
@@ -564,6 +585,11 @@ export class CoreBetterAuthController {
564
585
 
565
586
  /**
566
587
  * Extract session token from request
588
+ *
589
+ * Cookie priority (v11.12+):
590
+ * 1. Authorization: Bearer header
591
+ * 2. `{basePath}.session_token` (e.g., `iam.session_token`) - Better-Auth native
592
+ * 3. `token` - Legacy compatibility (only if Legacy Auth might be active)
567
593
  */
568
594
  protected extractSessionToken(req: Request): null | string {
569
595
  // Check Authorization header
@@ -572,10 +598,10 @@ export class CoreBetterAuthController {
572
598
  return authHeader.substring(7);
573
599
  }
574
600
 
575
- // Check cookies
601
+ // Check cookies - Better-Auth native cookie first, then legacy token
576
602
  const basePath = this.betterAuthService.getBasePath().replace(/^\//, '').replace(/\//g, '.');
577
603
  const cookieName = `${basePath}.session_token`;
578
- return req.cookies?.[cookieName] || req.cookies?.['better-auth.session_token'] || null;
604
+ return req.cookies?.[cookieName] || req.cookies?.['token'] || null;
579
605
  }
580
606
 
581
607
  /**
@@ -626,90 +652,38 @@ export class CoreBetterAuthController {
626
652
  /**
627
653
  * Process cookies for response
628
654
  *
629
- * Sets multiple cookies for authentication compatibility:
630
- *
631
- * | Cookie Name | Purpose |
632
- * |-------------|---------|
633
- * | `token` | Primary session token (nest-server compatibility) |
634
- * | `{basePath}.session_token` | Better Auth's native cookie for plugins (e.g., `iam.session_token`) |
635
- * | `better-auth.session_token` | Legacy Better Auth cookie name (backwards compatibility) |
636
- * | `{configured}` | Custom cookie name if configured via `options.advanced.cookies.session_token.name` |
637
- * | `session` | Session ID for reference/debugging |
655
+ * Sets multiple cookies for authentication compatibility using the centralized cookie helper.
656
+ * See BetterAuthCookieHelper for the complete cookie strategy.
638
657
  *
639
658
  * IMPORTANT: Better Auth's sign-in returns a session token in `result.token`.
640
659
  * This is NOT a JWT - it's the session token stored in the database.
641
660
  * The JWT plugin generates JWTs separately via the /token endpoint when needed.
642
661
  *
643
- * For plugins like Passkey to work, the session token must be available in a cookie
644
- * that Better Auth's plugin system recognizes (default: `{basePath}.session_token`).
645
- *
646
662
  * @param res - Express Response object
647
663
  * @param result - The CoreBetterAuthResponse to return
648
664
  * @param sessionToken - Optional session token to set in cookies (if not provided, uses result.token)
649
665
  */
650
666
  protected processCookies(res: Response, result: CoreBetterAuthResponse, sessionToken?: string): CoreBetterAuthResponse {
651
- // Check if cookie handling is activated
652
- if (this.configService.getFastButReadOnly('cookies')) {
653
- const cookieOptions = { httpOnly: true, sameSite: 'lax' as const, secure: process.env.NODE_ENV === 'production' };
654
-
655
- // Use provided sessionToken or fall back to result.token
656
- const tokenToSet = sessionToken || result.token;
657
-
658
- if (tokenToSet) {
659
- // Set the primary token cookie (for nest-server compatibility)
660
- res.cookie('token', tokenToSet, cookieOptions);
661
-
662
- // Set Better Auth's native session token cookies for plugin compatibility
663
- // This is CRITICAL for Passkey/WebAuthn to work
664
- const basePath = this.betterAuthService.getBasePath().replace(/^\//, '').replace(/\//g, '.');
665
- const defaultCookieName = `${basePath}.session_token`;
666
- res.cookie(defaultCookieName, tokenToSet, cookieOptions);
667
-
668
- // Also set the legacy cookie name for backwards compatibility
669
- res.cookie('better-auth.session_token', tokenToSet, cookieOptions);
670
-
671
- // Get configured cookie name and set if different from defaults
672
- const betterAuthConfig = this.configService.getFastButReadOnly('betterAuth');
673
- const configuredCookieName = betterAuthConfig?.options?.advanced?.cookies?.session_token?.name;
674
- if (configuredCookieName && configuredCookieName !== 'token' && configuredCookieName !== defaultCookieName) {
675
- res.cookie(configuredCookieName, tokenToSet, cookieOptions);
676
- }
667
+ const cookiesEnabled = this.configService.getFastButReadOnly('cookies') !== false;
677
668
 
678
- // Remove token from response body (it's now in cookies)
679
- if (result.token) {
680
- delete result.token;
681
- }
682
- }
683
-
684
- // Set session ID cookie (for reference/debugging)
685
- if (result.session) {
686
- res.cookie('session', result.session.id, cookieOptions);
669
+ // If a specific session token is provided, use it directly
670
+ if (sessionToken && cookiesEnabled) {
671
+ this.cookieHelper.setSessionCookies(res, sessionToken, result.session?.id);
672
+ if (result.token) {
673
+ delete result.token;
687
674
  }
675
+ return result;
688
676
  }
689
677
 
690
- return result;
678
+ // Otherwise, use the cookie helper's standard processing
679
+ return this.cookieHelper.processAuthResult(res, result, cookiesEnabled);
691
680
  }
692
681
 
693
682
  /**
694
- * Clear authentication cookies
683
+ * Clear authentication cookies using the centralized cookie helper.
695
684
  */
696
685
  protected clearAuthCookies(res: Response): void {
697
- const cookieOptions = { httpOnly: true, sameSite: 'lax' as const };
698
- res.cookie('token', '', { ...cookieOptions, maxAge: 0 });
699
- res.cookie('session', '', { ...cookieOptions, maxAge: 0 });
700
- res.cookie('better-auth.session_token', '', { ...cookieOptions, maxAge: 0 });
701
-
702
- // Clear the path-based session token cookie
703
- const basePath = this.betterAuthService.getBasePath().replace(/^\//, '').replace(/\//g, '.');
704
- const defaultCookieName = `${basePath}.session_token`;
705
- res.cookie(defaultCookieName, '', { ...cookieOptions, maxAge: 0 });
706
-
707
- // Clear configured session token cookie if different
708
- const betterAuthConfig = this.configService.getFastButReadOnly('betterAuth');
709
- const configuredCookieName = betterAuthConfig?.options?.advanced?.cookies?.session_token?.name;
710
- if (configuredCookieName && configuredCookieName !== 'token' && configuredCookieName !== defaultCookieName) {
711
- res.cookie(configuredCookieName, '', { ...cookieOptions, maxAge: 0 });
712
- }
686
+ this.cookieHelper.clearSessionCookies(res);
713
687
  }
714
688
 
715
689
  // ===================================================================================================================
@@ -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 { maskEmail, maskToken } from '../../common/helpers/logging.helper';
4
+ import { isLegacyJwt } from './core-better-auth-token.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';
@@ -80,10 +80,9 @@ export class CoreBetterAuthMiddleware implements NestMiddleware {
80
80
 
81
81
  // Check if token looks like a JWT (has 3 parts)
82
82
  if (tokenParts === 3) {
83
- // Decode JWT payload to check if it's a Legacy JWT or BetterAuth JWT
84
- // Legacy JWTs have 'id' claim, BetterAuth JWTs use 'sub'
85
- const isLegacyJwt = this.isLegacyJwt(token);
86
- if (isLegacyJwt) {
83
+ // Check if it's a Legacy JWT (has 'id' claim, no 'sub')
84
+ // Legacy JWTs should be handled by Passport, not Better-Auth
85
+ if (isLegacyJwt(token)) {
87
86
  // Legacy JWT - skip BetterAuth processing, let Passport handle it
88
87
  return next();
89
88
  }
@@ -139,27 +138,6 @@ export class CoreBetterAuthMiddleware implements NestMiddleware {
139
138
  next();
140
139
  }
141
140
 
142
- /**
143
- * Checks if a JWT token is a Legacy Auth JWT (has 'id' claim but no 'sub' claim)
144
- * Legacy JWTs use 'id' for user ID, BetterAuth JWTs use 'sub'
145
- */
146
- private isLegacyJwt(token: string): boolean {
147
- try {
148
- const parts = token.split('.');
149
- if (parts.length !== 3) return false;
150
-
151
- // Decode the payload (second part)
152
- const payloadStr = Buffer.from(parts[1], 'base64url').toString('utf-8');
153
- const payload = JSON.parse(payloadStr);
154
-
155
- // Legacy JWT has 'id' claim (and typically 'deviceId', 'tokenId')
156
- // BetterAuth JWT has 'sub' claim
157
- return payload.id !== undefined && payload.sub === undefined;
158
- } catch {
159
- return false;
160
- }
161
- }
162
-
163
141
  /**
164
142
  * Gets the session from Better-Auth using session token from cookies
165
143
  *
@@ -173,18 +151,11 @@ export class CoreBetterAuthMiddleware implements NestMiddleware {
173
151
  const basePath = this.betterAuthService.getBasePath();
174
152
  const sessionToken = extractSessionToken(req, basePath);
175
153
 
176
- this.logger.debug(`[MIDDLEWARE] getSession called, token found: ${sessionToken ? 'yes' : 'no'}`);
177
-
178
154
  if (sessionToken) {
179
- this.logger.debug(`[MIDDLEWARE] Found session token in cookies: ${maskToken(sessionToken)}`);
180
-
181
155
  // Use getSessionByToken to validate session directly from database
182
156
  const sessionResult = await this.betterAuthService.getSessionByToken(sessionToken);
183
157
 
184
- this.logger.debug(`[MIDDLEWARE] getSessionByToken result: user=${maskEmail(sessionResult?.user?.email)}, session=${!!sessionResult?.session}`);
185
-
186
158
  if (sessionResult?.user && sessionResult?.session) {
187
- this.logger.debug(`[MIDDLEWARE] Session validated for user: ${maskEmail(sessionResult.user.email)}`);
188
159
  return sessionResult as { session: any; user: BetterAuthSessionUser };
189
160
  }
190
161
  }
@@ -4,11 +4,12 @@ import { Request } from 'express';
4
4
  import { importJWK, jwtVerify } from 'jose';
5
5
  import { Connection } from 'mongoose';
6
6
 
7
- import { maskCookieHeader, maskEmail, maskToken } from '../../common/helpers/logging.helper';
7
+ import { 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';
11
11
  import { BetterAuthSessionUser } from './core-better-auth-user.mapper';
12
+ import { parseCookieHeader, signCookieValueIfNeeded } from './core-better-auth-web.helper';
12
13
  import { BETTER_AUTH_INSTANCE } from './core-better-auth.module';
13
14
 
14
15
  /**
@@ -316,14 +317,35 @@ export class CoreBetterAuthService {
316
317
  }
317
318
  }
318
319
 
319
- // Debug: Log the cookie header being sent to api.getSession (masked for security)
320
+ // Sign cookies before sending to Better-Auth API
321
+ // Browser clients send unsigned cookies, but Better-Auth expects signed cookies
320
322
  const cookieHeader = headers.get('cookie');
321
- this.logger.debug(`getSession called with cookies: ${maskCookieHeader(cookieHeader)}`);
323
+ if (cookieHeader && this.config?.secret) {
324
+ const basePath = this.getBasePath()?.replace(/^\//, '').replace(/\//g, '.') || 'iam';
325
+ const sessionCookieName = `${basePath}.session_token`;
326
+ const cookies = parseCookieHeader(cookieHeader);
327
+ let modified = false;
328
+
329
+ for (const [name, value] of Object.entries(cookies)) {
330
+ // Sign the session token cookie if it's not already signed
331
+ if (name === sessionCookieName || name === 'token') {
332
+ const signedValue = signCookieValueIfNeeded(value, this.config.secret);
333
+ if (signedValue !== value) {
334
+ cookies[name] = signedValue;
335
+ modified = true;
336
+ }
337
+ }
338
+ }
322
339
 
323
- const response = await api.getSession({ headers });
340
+ if (modified) {
341
+ const signedCookieHeader = Object.entries(cookies)
342
+ .map(([name, value]) => `${name}=${value}`)
343
+ .join('; ');
344
+ headers.set('cookie', signedCookieHeader);
345
+ }
346
+ }
324
347
 
325
- // Debug: Log the response from api.getSession
326
- this.logger.debug(`getSession response: ${JSON.stringify(response)?.substring(0, 200)}`);
348
+ const response = await api.getSession({ headers });
327
349
 
328
350
  if (response && typeof response === 'object' && 'user' in response) {
329
351
  return response as SessionResult;
@@ -347,11 +369,11 @@ export class CoreBetterAuthService {
347
369
  *
348
370
  * @example
349
371
  * ```typescript
350
- * // Get session token from cookie or header
351
- * const sessionToken = req.cookies['better-auth.session_token'];
372
+ * // Get session token from cookie (using basePath-based name)
373
+ * const sessionToken = req.cookies['iam.session_token'];
352
374
  * const success = await betterAuthService.revokeSession(sessionToken);
353
375
  * if (success) {
354
- * res.clearCookie('better-auth.session_token');
376
+ * res.clearCookie('iam.session_token');
355
377
  * }
356
378
  * ```
357
379
  */
@@ -28,10 +28,12 @@ export * from './better-auth.resolver';
28
28
  export * from './better-auth.types';
29
29
  export * from './core-better-auth-api.middleware';
30
30
  export * from './core-better-auth-auth.model';
31
+ export * from './core-better-auth-cookie.helper';
31
32
  export * from './core-better-auth-migration-status.model';
32
33
  export * from './core-better-auth-models';
33
34
  export * from './core-better-auth-rate-limit.middleware';
34
35
  export * from './core-better-auth-rate-limiter.service';
36
+ export * from './core-better-auth-token.helper';
35
37
  export * from './core-better-auth-user.mapper';
36
38
  export * from './core-better-auth-web.helper';
37
39
  export * from './core-better-auth.controller';