@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.
- package/dist/core/modules/auth/core-auth.controller.js +1 -1
- package/dist/core/modules/auth/core-auth.controller.js.map +1 -1
- package/dist/core/modules/auth/core-auth.resolver.js +1 -1
- package/dist/core/modules/auth/core-auth.resolver.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth-token.service.js +1 -4
- package/dist/core/modules/better-auth/better-auth-token.service.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.config.js +55 -8
- package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-api.middleware.d.ts +2 -0
- package/dist/core/modules/better-auth/core-better-auth-api.middleware.js +22 -18
- 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 +41 -0
- package/dist/core/modules/better-auth/core-better-auth-cookie.helper.js +107 -0
- package/dist/core/modules/better-auth/core-better-auth-cookie.helper.js.map +1 -0
- package/dist/core/modules/better-auth/core-better-auth-token.helper.d.ts +16 -0
- package/dist/core/modules/better-auth/core-better-auth-token.helper.js +66 -0
- package/dist/core/modules/better-auth/core-better-auth-token.helper.js.map +1 -0
- package/dist/core/modules/better-auth/core-better-auth-web.helper.d.ts +2 -3
- package/dist/core/modules/better-auth/core-better-auth-web.helper.js +33 -22
- package/dist/core/modules/better-auth/core-better-auth-web.helper.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.controller.d.ts +2 -0
- package/dist/core/modules/better-auth/core-better-auth.controller.js +17 -34
- package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.middleware.d.ts +0 -1
- package/dist/core/modules/better-auth/core-better-auth.middleware.js +2 -20
- package/dist/core/modules/better-auth/core-better-auth.middleware.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.service.js +22 -2
- package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
- package/dist/core/modules/better-auth/index.d.ts +2 -0
- package/dist/core/modules/better-auth/index.js +2 -0
- package/dist/core/modules/better-auth/index.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/src/core/modules/auth/core-auth.controller.ts +2 -2
- package/src/core/modules/auth/core-auth.resolver.ts +2 -2
- package/src/core/modules/better-auth/better-auth-token.service.ts +5 -8
- package/src/core/modules/better-auth/better-auth.config.ts +139 -12
- package/src/core/modules/better-auth/core-better-auth-api.middleware.ts +44 -20
- package/src/core/modules/better-auth/core-better-auth-cookie.helper.ts +323 -0
- package/src/core/modules/better-auth/core-better-auth-token.helper.ts +200 -0
- package/src/core/modules/better-auth/core-better-auth-web.helper.ts +73 -36
- package/src/core/modules/better-auth/core-better-auth.controller.ts +43 -69
- package/src/core/modules/better-auth/core-better-auth.middleware.ts +4 -33
- package/src/core/modules/better-auth/core-better-auth.service.ts +31 -9
- 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
|
-
*
|
|
41
|
-
* 1.
|
|
42
|
-
* 2. `
|
|
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
|
|
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
|
-
|
|
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`, //
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
* @
|
|
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
|
|
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
|
|
225
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
286
|
-
|
|
287
|
-
|
|
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
|
|
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?.['
|
|
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
|
-
|
|
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
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
//
|
|
84
|
-
// Legacy JWTs
|
|
85
|
-
|
|
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 {
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
351
|
-
* const sessionToken = req.cookies['
|
|
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('
|
|
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';
|