@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenne.tech/nest-server",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.12.0",
|
|
4
4
|
"description": "Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"node",
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
"cookie-parser": "1.4.7",
|
|
106
106
|
"dotenv": "17.2.3",
|
|
107
107
|
"ejs": "3.1.10",
|
|
108
|
-
"express": "5.1
|
|
108
|
+
"express": "5.2.1",
|
|
109
109
|
"graphql": "16.12.0",
|
|
110
110
|
"graphql-query-complexity": "1.1.0",
|
|
111
111
|
"graphql-subscriptions": "3.0.0",
|
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
"mongoose": "9.1.5",
|
|
118
118
|
"multer": "2.0.2",
|
|
119
119
|
"node-mailjet": "6.0.11",
|
|
120
|
-
"nodemailer": "7.0.
|
|
120
|
+
"nodemailer": "7.0.13",
|
|
121
121
|
"passport": "0.7.0",
|
|
122
122
|
"passport-jwt": "4.0.1",
|
|
123
123
|
"reflect-metadata": "0.2.2",
|
|
@@ -131,20 +131,20 @@
|
|
|
131
131
|
"@nestjs/cli": "11.0.16",
|
|
132
132
|
"@nestjs/schematics": "11.0.9",
|
|
133
133
|
"@nestjs/testing": "11.1.12",
|
|
134
|
-
"@swc/cli": "0.7.
|
|
135
|
-
"@swc/core": "1.15.
|
|
134
|
+
"@swc/cli": "0.7.10",
|
|
135
|
+
"@swc/core": "1.15.11",
|
|
136
136
|
"@types/compression": "1.8.1",
|
|
137
137
|
"@types/cookie-parser": "1.4.10",
|
|
138
138
|
"@types/ejs": "3.1.5",
|
|
139
139
|
"@types/express": "4.17.21",
|
|
140
140
|
"@types/lodash": "4.17.23",
|
|
141
141
|
"@types/multer": "2.0.0",
|
|
142
|
-
"@types/node": "25.0
|
|
143
|
-
"@types/nodemailer": "7.0.
|
|
142
|
+
"@types/node": "25.2.0",
|
|
143
|
+
"@types/nodemailer": "7.0.9",
|
|
144
144
|
"@types/passport": "1.0.17",
|
|
145
145
|
"@types/supertest": "6.0.3",
|
|
146
|
-
"@typescript-eslint/eslint-plugin": "8.
|
|
147
|
-
"@typescript-eslint/parser": "8.
|
|
146
|
+
"@typescript-eslint/eslint-plugin": "8.54.0",
|
|
147
|
+
"@typescript-eslint/parser": "8.54.0",
|
|
148
148
|
"@vitest/coverage-v8": "4.0.18",
|
|
149
149
|
"@vitest/ui": "4.0.18",
|
|
150
150
|
"ansi-colors": "4.1.3",
|
|
@@ -162,7 +162,7 @@
|
|
|
162
162
|
"npm-watch": "0.13.0",
|
|
163
163
|
"otpauth": "9.4.1",
|
|
164
164
|
"pm2": "6.0.14",
|
|
165
|
-
"prettier": "3.
|
|
165
|
+
"prettier": "3.8.1",
|
|
166
166
|
"pretty-quick": "4.2.2",
|
|
167
167
|
"rimraf": "6.1.2",
|
|
168
168
|
"supertest": "7.2.2",
|
|
@@ -190,8 +190,8 @@ export class CoreAuthController {
|
|
|
190
190
|
* Process cookies
|
|
191
191
|
*/
|
|
192
192
|
protected processCookies(res: ResponseType, result: any) {
|
|
193
|
-
// Check if cookie handling is activated
|
|
194
|
-
if (this.configService.getFastButReadOnly('cookies')) {
|
|
193
|
+
// Check if cookie handling is activated (enabled by default, unless explicitly set to false)
|
|
194
|
+
if (this.configService.getFastButReadOnly('cookies') !== false) {
|
|
195
195
|
// Set cookies
|
|
196
196
|
if (!result || typeof result !== 'object') {
|
|
197
197
|
res.cookie('token', '', { httpOnly: true });
|
|
@@ -172,8 +172,8 @@ export class CoreAuthResolver {
|
|
|
172
172
|
* Process cookies
|
|
173
173
|
*/
|
|
174
174
|
protected processCookies(ctx: { res: ResponseType }, result: any) {
|
|
175
|
-
// Check if cookie handling is activated
|
|
176
|
-
if (this.configService.getFastButReadOnly('cookies')) {
|
|
175
|
+
// Check if cookie handling is activated (enabled by default, unless explicitly set to false)
|
|
176
|
+
if (this.configService.getFastButReadOnly('cookies') !== false) {
|
|
177
177
|
// Set cookies
|
|
178
178
|
if (!result || typeof result !== 'object') {
|
|
179
179
|
ctx.res.cookie('token', '', { httpOnly: true });
|
|
@@ -49,9 +49,10 @@ export class BetterAuthTokenService {
|
|
|
49
49
|
/**
|
|
50
50
|
* Extracts a token from the request's Authorization header or cookies.
|
|
51
51
|
*
|
|
52
|
-
*
|
|
52
|
+
* Cookie priority (v11.12+):
|
|
53
53
|
* 1. Authorization header (Bearer token)
|
|
54
|
-
* 2.
|
|
54
|
+
* 2. `{basePath}.session_token` (e.g., `iam.session_token`) - Better-Auth native
|
|
55
|
+
* 3. `token` - Legacy nest-server cookie
|
|
55
56
|
*
|
|
56
57
|
* @param request - HTTP request object with headers and cookies
|
|
57
58
|
* @returns Token extraction result with token and source
|
|
@@ -70,14 +71,10 @@ export class BetterAuthTokenService {
|
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
// Try cookies
|
|
74
|
+
// Try cookies - Better-Auth native cookie first, then legacy
|
|
74
75
|
if (request.cookies && this.betterAuthService) {
|
|
75
76
|
const cookieName = this.betterAuthService.getSessionCookieName();
|
|
76
|
-
const token =
|
|
77
|
-
request.cookies[cookieName] ||
|
|
78
|
-
request.cookies['better-auth.session_token'] ||
|
|
79
|
-
request.cookies['token'] ||
|
|
80
|
-
undefined;
|
|
77
|
+
const token = request.cookies[cookieName] || request.cookies['token'] || undefined;
|
|
81
78
|
|
|
82
79
|
if (token) {
|
|
83
80
|
return { source: 'cookie', token };
|
|
@@ -4,6 +4,8 @@ import { betterAuth, BetterAuthPlugin } from 'better-auth';
|
|
|
4
4
|
import { mongodbAdapter } from 'better-auth/adapters/mongodb';
|
|
5
5
|
import { jwt, twoFactor } from 'better-auth/plugins';
|
|
6
6
|
import * as crypto from 'crypto';
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
7
9
|
|
|
8
10
|
import { IBetterAuth } from '../../common/interfaces/server-options.interface';
|
|
9
11
|
|
|
@@ -27,10 +29,16 @@ function generateSecureSecret(): string {
|
|
|
27
29
|
|
|
28
30
|
/**
|
|
29
31
|
* Cached auto-generated secret for the current server instance.
|
|
30
|
-
* Generated once at module load to ensure consistency within a single run.
|
|
32
|
+
* Generated once at a module load to ensure consistency within a single run.
|
|
31
33
|
*/
|
|
32
34
|
let cachedAutoGeneratedSecret: null | string = null;
|
|
33
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Cached project app name for the current server instance.
|
|
38
|
+
* Read once from package.json to avoid repeated file reads.
|
|
39
|
+
*/
|
|
40
|
+
let cachedProjectAppName: null | string = null;
|
|
41
|
+
|
|
34
42
|
/**
|
|
35
43
|
* Options for creating a better-auth instance
|
|
36
44
|
*/
|
|
@@ -165,7 +173,7 @@ interface ValidationResult {
|
|
|
165
173
|
*/
|
|
166
174
|
export function createBetterAuthInstance(options: CreateBetterAuthOptions): BetterAuthInstance | null {
|
|
167
175
|
const logger = new Logger('BetterAuthConfig');
|
|
168
|
-
const { config, db, fallbackSecrets } = options;
|
|
176
|
+
const { config, db, fallbackSecrets, serverEnv } = options;
|
|
169
177
|
|
|
170
178
|
// Return null only if better-auth is explicitly disabled
|
|
171
179
|
// BetterAuth is enabled by default (zero-config)
|
|
@@ -184,7 +192,7 @@ export function createBetterAuthInstance(options: CreateBetterAuthOptions): Bett
|
|
|
184
192
|
// Normalize Passkey configuration with auto-detection from resolved URLs
|
|
185
193
|
// This must happen BEFORE validation to allow graceful degradation
|
|
186
194
|
// Passkey is now AUTO-ACTIVATED by default (not opt-in)
|
|
187
|
-
const passkeyNormalization = normalizePasskeyConfig(config, resolvedUrls);
|
|
195
|
+
const passkeyNormalization = normalizePasskeyConfig(config, { resolvedUrls, serverEnv });
|
|
188
196
|
|
|
189
197
|
// Log passkey normalization warnings (info about auto-detection or disabled status)
|
|
190
198
|
for (const warning of passkeyNormalization.warnings) {
|
|
@@ -205,15 +213,23 @@ export function createBetterAuthInstance(options: CreateBetterAuthOptions): Bett
|
|
|
205
213
|
}
|
|
206
214
|
|
|
207
215
|
// Build configuration components (pass normalized passkey config and resolved URLs)
|
|
208
|
-
const plugins = buildPlugins(config, passkeyNormalization);
|
|
216
|
+
const plugins = buildPlugins(config, { passkeyNormalization, serverEnv });
|
|
209
217
|
const socialProviders = buildSocialProviders(config);
|
|
210
218
|
const trustedOrigins = buildTrustedOrigins(config, passkeyNormalization, resolvedUrls);
|
|
211
219
|
const additionalFields = buildUserFields(config);
|
|
212
220
|
|
|
213
221
|
// Build the base Better-Auth configuration
|
|
214
222
|
// Use resolved baseUrl (with local defaults) or fallback
|
|
223
|
+
const basePath = config.basePath || '/iam';
|
|
224
|
+
// Cookie prefix derived from basePath (e.g., '/iam' → 'iam')
|
|
225
|
+
// This ensures Better-Auth looks for cookies like 'iam.session_token' instead of 'better-auth.session_token'
|
|
226
|
+
const cookiePrefix = basePath.replace(/^\//, '').replace(/\//g, '.');
|
|
227
|
+
|
|
215
228
|
const betterAuthConfig: Record<string, unknown> = {
|
|
216
|
-
|
|
229
|
+
advanced: {
|
|
230
|
+
cookiePrefix,
|
|
231
|
+
},
|
|
232
|
+
basePath,
|
|
217
233
|
baseURL: resolvedUrls.baseUrl || config.baseUrl || 'http://localhost:3000',
|
|
218
234
|
database: mongodbAdapter(db),
|
|
219
235
|
// Enable email/password authentication by default (required by Better-Auth 1.x)
|
|
@@ -257,9 +273,15 @@ export function createBetterAuthInstance(options: CreateBetterAuthOptions): Bett
|
|
|
257
273
|
* - `undefined`: Disabled (default)
|
|
258
274
|
*
|
|
259
275
|
* @param config - Better-auth configuration
|
|
260
|
-
* @param
|
|
276
|
+
* @param options - Additional options
|
|
277
|
+
* @param options.passkeyNormalization - Normalized passkey configuration from normalizePasskeyConfig()
|
|
278
|
+
* @param options.serverEnv - Server environment for auto-detected app name suffix
|
|
261
279
|
*/
|
|
262
|
-
function buildPlugins(
|
|
280
|
+
function buildPlugins(
|
|
281
|
+
config: IBetterAuth,
|
|
282
|
+
options: { passkeyNormalization: PasskeyNormalizationResult; serverEnv?: string },
|
|
283
|
+
): BetterAuthPlugin[] {
|
|
284
|
+
const { passkeyNormalization, serverEnv } = options;
|
|
263
285
|
const plugins: BetterAuthPlugin[] = [];
|
|
264
286
|
|
|
265
287
|
// JWT Plugin for API client compatibility
|
|
@@ -285,7 +307,8 @@ function buildPlugins(config: IBetterAuth, passkeyNormalization: PasskeyNormaliz
|
|
|
285
307
|
const twoFactorConfig = typeof config.twoFactor === 'object' ? config.twoFactor : {};
|
|
286
308
|
plugins.push(
|
|
287
309
|
twoFactor({
|
|
288
|
-
issuer:
|
|
310
|
+
// issuer: Use explicit config, or auto-detect from package.json with environment suffix
|
|
311
|
+
issuer: twoFactorConfig.appName || getProjectAppName(serverEnv),
|
|
289
312
|
}),
|
|
290
313
|
);
|
|
291
314
|
}
|
|
@@ -447,6 +470,50 @@ function buildUserFields(config: IBetterAuth): Record<string, UserFieldConfig> {
|
|
|
447
470
|
return coreFields;
|
|
448
471
|
}
|
|
449
472
|
|
|
473
|
+
/**
|
|
474
|
+
* Formats the environment name as a suffix.
|
|
475
|
+
* Only adds suffix for development/test environments.
|
|
476
|
+
*
|
|
477
|
+
* @example
|
|
478
|
+
* formatEnvSuffix('local') // → '(Local)'
|
|
479
|
+
* formatEnvSuffix('development') // → '(Development)'
|
|
480
|
+
* formatEnvSuffix('test') // → '(Test)'
|
|
481
|
+
* formatEnvSuffix('production') // → '' (no suffix for production)
|
|
482
|
+
* formatEnvSuffix(undefined) // → ''
|
|
483
|
+
*/
|
|
484
|
+
function formatEnvSuffix(env?: string): string {
|
|
485
|
+
if (!env) return '';
|
|
486
|
+
|
|
487
|
+
// Don't add suffix for production
|
|
488
|
+
if (env === 'production' || env === 'prod') return '';
|
|
489
|
+
|
|
490
|
+
// Capitalize first letter
|
|
491
|
+
const formatted = env.charAt(0).toUpperCase() + env.slice(1).toLowerCase();
|
|
492
|
+
return `(${formatted})`;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Formats a package name to a human-readable display name.
|
|
497
|
+
* Converts kebab-case and snake_case to Title Case.
|
|
498
|
+
*
|
|
499
|
+
* @example
|
|
500
|
+
* formatProjectName('my-awesome-app') // → 'My Awesome App'
|
|
501
|
+
* formatProjectName('@org/my-app') // → 'My App'
|
|
502
|
+
* formatProjectName('nest_server_starter') // → 'Nest Server Starter'
|
|
503
|
+
*/
|
|
504
|
+
function formatProjectName(name: string): string {
|
|
505
|
+
// Remove scope (e.g., '@org/my-app' → 'my-app')
|
|
506
|
+
let formatted = name.replace(/^@[^/]+\//, '');
|
|
507
|
+
|
|
508
|
+
// Split by hyphens and underscores, capitalize each word
|
|
509
|
+
formatted = formatted
|
|
510
|
+
.split(/[-_]/)
|
|
511
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
512
|
+
.join(' ');
|
|
513
|
+
|
|
514
|
+
return formatted;
|
|
515
|
+
}
|
|
516
|
+
|
|
450
517
|
/**
|
|
451
518
|
* Gets or generates the fallback secret for development.
|
|
452
519
|
* The secret is cached to ensure consistency during the server's lifetime.
|
|
@@ -458,6 +525,37 @@ function getAutoGeneratedSecret(): string {
|
|
|
458
525
|
return cachedAutoGeneratedSecret;
|
|
459
526
|
}
|
|
460
527
|
|
|
528
|
+
/**
|
|
529
|
+
* Gets the project's app name from package.json.
|
|
530
|
+
*
|
|
531
|
+
* This function reads the `name` field from the project's package.json
|
|
532
|
+
* and formats it for display (converts a kebab-case to Title Case).
|
|
533
|
+
*
|
|
534
|
+
* Used as default for:
|
|
535
|
+
* - `betterAuth.passkey.rpName` (displayed in browser Passkey prompts)
|
|
536
|
+
* - `betterAuth.twoFactor.appName` (displayed in authenticator apps)
|
|
537
|
+
*
|
|
538
|
+
* @param serverEnv - Optional server environment to append as suffix (e.g., 'local' → '(Local)')
|
|
539
|
+
* @returns The formatted project name, or 'Nest Server' as fallback
|
|
540
|
+
*
|
|
541
|
+
* @example
|
|
542
|
+
* // package.json: { "name": "my-awesome-app" }
|
|
543
|
+
* getProjectAppName() // → 'My Awesome App'
|
|
544
|
+
* getProjectAppName('local') // → 'My Awesome App (Local)'
|
|
545
|
+
* getProjectAppName('test') // → 'My Awesome App (Test)'
|
|
546
|
+
*/
|
|
547
|
+
function getProjectAppName(serverEnv?: string): string {
|
|
548
|
+
// Return cached value if available (without env suffix, will be added after)
|
|
549
|
+
if (cachedProjectAppName === null) {
|
|
550
|
+
cachedProjectAppName = readProjectNameFromPackageJson();
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Add environment suffix for non-production environments
|
|
554
|
+
// This helps developers distinguish Passkeys when running multiple projects locally
|
|
555
|
+
const envSuffix = formatEnvSuffix(serverEnv);
|
|
556
|
+
return envSuffix ? `${cachedProjectAppName} ${envSuffix}` : cachedProjectAppName;
|
|
557
|
+
}
|
|
558
|
+
|
|
461
559
|
/**
|
|
462
560
|
* Checks if a secret has valid minimum length (32 characters)
|
|
463
561
|
*/
|
|
@@ -477,6 +575,28 @@ function isValidUrl(url: string): boolean {
|
|
|
477
575
|
}
|
|
478
576
|
}
|
|
479
577
|
|
|
578
|
+
/**
|
|
579
|
+
* Reads the project name from package.json in the current working directory.
|
|
580
|
+
* Falls back to 'Nest Server' if package.json cannot be read or has no name.
|
|
581
|
+
*/
|
|
582
|
+
function readProjectNameFromPackageJson(): string {
|
|
583
|
+
const fallback = 'Nest Server';
|
|
584
|
+
|
|
585
|
+
try {
|
|
586
|
+
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
|
587
|
+
const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf-8');
|
|
588
|
+
const packageJson = JSON.parse(packageJsonContent);
|
|
589
|
+
|
|
590
|
+
if (packageJson.name && typeof packageJson.name === 'string') {
|
|
591
|
+
return formatProjectName(packageJson.name);
|
|
592
|
+
}
|
|
593
|
+
} catch {
|
|
594
|
+
// Ignore errors - use fallback
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return fallback;
|
|
598
|
+
}
|
|
599
|
+
|
|
480
600
|
/**
|
|
481
601
|
* Default URLs for local/test environments (local, ci, e2e)
|
|
482
602
|
* These environments typically run on localhost and don't have a deployed domain.
|
|
@@ -598,10 +718,16 @@ function extractRpIdFromUrl(url: string): string {
|
|
|
598
718
|
* - `trustedOrigins`: from config.trustedOrigins > [resolvedUrls.appUrl]
|
|
599
719
|
*
|
|
600
720
|
* @param config - Better-auth configuration
|
|
601
|
-
* @param
|
|
721
|
+
* @param options - Additional options
|
|
722
|
+
* @param options.resolvedUrls - Resolved URLs from resolveUrls()
|
|
723
|
+
* @param options.serverEnv - Server environment for auto-detected app name suffix
|
|
602
724
|
* @returns Normalization result with enabled status, config, and warnings
|
|
603
725
|
*/
|
|
604
|
-
function normalizePasskeyConfig(
|
|
726
|
+
function normalizePasskeyConfig(
|
|
727
|
+
config: IBetterAuth,
|
|
728
|
+
options: { resolvedUrls: ResolvedUrls; serverEnv?: string },
|
|
729
|
+
): PasskeyNormalizationResult {
|
|
730
|
+
const { resolvedUrls, serverEnv } = options;
|
|
605
731
|
const warnings: string[] = [];
|
|
606
732
|
|
|
607
733
|
// Check if Passkey is explicitly DISABLED
|
|
@@ -657,10 +783,11 @@ function normalizePasskeyConfig(config: IBetterAuth, resolvedUrls: ResolvedUrls)
|
|
|
657
783
|
}
|
|
658
784
|
|
|
659
785
|
// Build normalized config
|
|
786
|
+
// rpName: Use explicit config, or auto-detect from package.json with environment suffix
|
|
660
787
|
const normalizedConfig: NormalizedPasskeyConfig = {
|
|
661
788
|
origin: finalOrigin,
|
|
662
789
|
rpId: finalRpId,
|
|
663
|
-
rpName: rawConfig.rpName ||
|
|
790
|
+
rpName: rawConfig.rpName || getProjectAppName(serverEnv),
|
|
664
791
|
};
|
|
665
792
|
|
|
666
793
|
// Copy optional fields from explicit config
|
|
@@ -853,7 +980,7 @@ function validateConfig(
|
|
|
853
980
|
errors.push(`Social provider '${name}' is missing clientSecret`);
|
|
854
981
|
}
|
|
855
982
|
} else {
|
|
856
|
-
// No credentials provided but provider is configured and not disabled
|
|
983
|
+
// No credentials provided, but the provider is configured and not disabled
|
|
857
984
|
// This is likely a configuration mistake - warn the user
|
|
858
985
|
warnings.push(
|
|
859
986
|
`Social provider '${name}' is configured but missing both clientId and clientSecret. ` +
|
|
@@ -3,6 +3,7 @@ import { NextFunction, Request, Response } from 'express';
|
|
|
3
3
|
|
|
4
4
|
import { isProduction } from '../../common/helpers/logging.helper';
|
|
5
5
|
import { CoreBetterAuthChallengeService } from './core-better-auth-challenge.service';
|
|
6
|
+
import { BetterAuthCookieHelper, createCookieHelper } from './core-better-auth-cookie.helper';
|
|
6
7
|
import { extractSessionToken, sendWebResponse, signCookieValue, toWebRequest } from './core-better-auth-web.helper';
|
|
7
8
|
import { CoreBetterAuthService } from './core-better-auth.service';
|
|
8
9
|
|
|
@@ -19,28 +20,17 @@ import { CoreBetterAuthService } from './core-better-auth.service';
|
|
|
19
20
|
* All other paths (Passkey, 2FA, etc.) go directly to Better Auth's
|
|
20
21
|
* native handler via this middleware for maximum compatibility.
|
|
21
22
|
*/
|
|
22
|
-
const CONTROLLER_HANDLED_PATHS = [
|
|
23
|
-
'/sign-in/email',
|
|
24
|
-
'/sign-up/email',
|
|
25
|
-
'/sign-out',
|
|
26
|
-
'/session',
|
|
27
|
-
];
|
|
23
|
+
const CONTROLLER_HANDLED_PATHS = ['/sign-in/email', '/sign-up/email', '/sign-out', '/session'];
|
|
28
24
|
|
|
29
25
|
/**
|
|
30
26
|
* Passkey paths that generate challenges
|
|
31
27
|
*/
|
|
32
|
-
const PASSKEY_GENERATE_PATHS = [
|
|
33
|
-
'/passkey/generate-register-options',
|
|
34
|
-
'/passkey/generate-authenticate-options',
|
|
35
|
-
];
|
|
28
|
+
const PASSKEY_GENERATE_PATHS = ['/passkey/generate-register-options', '/passkey/generate-authenticate-options'];
|
|
36
29
|
|
|
37
30
|
/**
|
|
38
31
|
* Passkey paths that verify challenges
|
|
39
32
|
*/
|
|
40
|
-
const PASSKEY_VERIFY_PATHS = [
|
|
41
|
-
'/passkey/verify-registration',
|
|
42
|
-
'/passkey/verify-authentication',
|
|
43
|
-
];
|
|
33
|
+
const PASSKEY_VERIFY_PATHS = ['/passkey/verify-registration', '/passkey/verify-authentication'];
|
|
44
34
|
|
|
45
35
|
/**
|
|
46
36
|
* Middleware that forwards Better Auth API requests to the native Better Auth handler.
|
|
@@ -63,12 +53,36 @@ export class CoreBetterAuthApiMiddleware implements NestMiddleware {
|
|
|
63
53
|
private readonly logger = new Logger(CoreBetterAuthApiMiddleware.name);
|
|
64
54
|
private readonly isProd = isProduction();
|
|
65
55
|
private loggedChallengeStorageMode = false;
|
|
56
|
+
private cookieHelper?: BetterAuthCookieHelper;
|
|
66
57
|
|
|
67
58
|
constructor(
|
|
68
59
|
private readonly betterAuthService: CoreBetterAuthService,
|
|
69
60
|
@Optional() private readonly challengeService?: CoreBetterAuthChallengeService,
|
|
70
61
|
) {}
|
|
71
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Gets or creates the cookie helper instance.
|
|
65
|
+
* Lazy initialization because betterAuthService may not be fully initialized in constructor.
|
|
66
|
+
*
|
|
67
|
+
* Note: Legacy cookie is not enabled in middleware since we can't access ConfigService.
|
|
68
|
+
* The middleware only needs to set the native Better-Auth cookie.
|
|
69
|
+
* Secret is required for cookie signing (Passkey/2FA).
|
|
70
|
+
*/
|
|
71
|
+
private getCookieHelper(): BetterAuthCookieHelper {
|
|
72
|
+
if (!this.cookieHelper) {
|
|
73
|
+
const config = this.betterAuthService.getConfig();
|
|
74
|
+
this.cookieHelper = createCookieHelper(
|
|
75
|
+
this.betterAuthService.getBasePath(),
|
|
76
|
+
{
|
|
77
|
+
legacyCookieEnabled: false, // Middleware doesn't need legacy cookie
|
|
78
|
+
secret: config?.secret, // Required for cookie signing
|
|
79
|
+
},
|
|
80
|
+
this.logger,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return this.cookieHelper;
|
|
84
|
+
}
|
|
85
|
+
|
|
72
86
|
/**
|
|
73
87
|
* Check if database challenge storage should be used.
|
|
74
88
|
* This is checked dynamically because the ChallengeService initializes in onModuleInit.
|
|
@@ -134,7 +148,9 @@ export class CoreBetterAuthApiMiddleware implements NestMiddleware {
|
|
|
134
148
|
let challengeIdToDelete: string | undefined;
|
|
135
149
|
if (isPasskeyVerify && this.challengeService) {
|
|
136
150
|
const challengeId = req.body?.challengeId;
|
|
137
|
-
this.logger.debug(
|
|
151
|
+
this.logger.debug(
|
|
152
|
+
`Passkey verify: challengeId=${challengeId ? `${challengeId.substring(0, 8)}...` : 'MISSING'}, body keys=${Object.keys(req.body || {}).join(', ')}`,
|
|
153
|
+
);
|
|
138
154
|
if (challengeId) {
|
|
139
155
|
const verificationToken = await this.challengeService.getVerificationToken(challengeId);
|
|
140
156
|
if (verificationToken) {
|
|
@@ -244,9 +260,19 @@ export class CoreBetterAuthApiMiddleware implements NestMiddleware {
|
|
|
244
260
|
}
|
|
245
261
|
}
|
|
246
262
|
|
|
247
|
-
// Clean up the used challenge mapping after verification
|
|
248
|
-
|
|
263
|
+
// Clean up the used challenge mapping only after SUCCESSFUL verification
|
|
264
|
+
// On failure, keep the challenge so the user can retry with a different passkey
|
|
265
|
+
if (challengeIdToDelete && this.challengeService && response.ok) {
|
|
249
266
|
await this.challengeService.deleteChallengeMapping(challengeIdToDelete);
|
|
267
|
+
} else if (challengeIdToDelete && !response.ok) {
|
|
268
|
+
this.logger.debug(`Keeping challenge mapping after failed verification (status=${response.status}) for retry`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// For successful passkey verify-authentication, set session cookie
|
|
272
|
+
// Better-Auth's native handler sets its own cookies, but we extract and
|
|
273
|
+
// re-set them using our cookie helper for consistent cookie handling.
|
|
274
|
+
if (relativePath === '/passkey/verify-authentication' && response.ok) {
|
|
275
|
+
this.getCookieHelper().setSessionCookiesFromWebResponse(res, response);
|
|
250
276
|
}
|
|
251
277
|
|
|
252
278
|
// Convert Web Standard Response to Express response using shared helper
|
|
@@ -261,9 +287,7 @@ export class CoreBetterAuthApiMiddleware implements NestMiddleware {
|
|
|
261
287
|
|
|
262
288
|
// Send error response if headers not sent
|
|
263
289
|
if (!res.headersSent) {
|
|
264
|
-
const message = this.isProd
|
|
265
|
-
? 'Authentication error'
|
|
266
|
-
: (error instanceof Error ? error.message : 'Unknown error');
|
|
290
|
+
const message = this.isProd ? 'Authentication error' : error instanceof Error ? error.message : 'Unknown error';
|
|
267
291
|
res.status(500).json({
|
|
268
292
|
error: 'Authentication handler error',
|
|
269
293
|
message,
|