@lenne.tech/nest-server 11.6.1 → 11.6.2
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/config.env.js +141 -0
- package/dist/config.env.js.map +1 -1
- package/dist/core/common/decorators/graphql-populate.decorator.d.ts +2 -2
- package/dist/core/common/decorators/restricted.decorator.d.ts +1 -0
- package/dist/core/common/decorators/restricted.decorator.js +1 -1
- package/dist/core/common/decorators/restricted.decorator.js.map +1 -1
- package/dist/core/common/helpers/input.helper.d.ts +1 -0
- package/dist/core/common/helpers/input.helper.js +1 -1
- package/dist/core/common/helpers/input.helper.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +50 -0
- package/dist/core/modules/auth/auth-guard-strategy.enum.d.ts +1 -0
- package/dist/core/modules/auth/auth-guard-strategy.enum.js +1 -0
- package/dist/core/modules/auth/auth-guard-strategy.enum.js.map +1 -1
- package/dist/core/modules/auth/guards/auth.guard.js +11 -5
- package/dist/core/modules/auth/guards/auth.guard.js.map +1 -1
- package/dist/core/modules/auth/tokens.decorator.d.ts +1 -1
- package/dist/core/modules/better-auth/better-auth-auth.model.d.ts +9 -0
- package/dist/core/modules/better-auth/better-auth-auth.model.js +63 -0
- package/dist/core/modules/better-auth/better-auth-auth.model.js.map +1 -0
- package/dist/core/modules/better-auth/better-auth-models.d.ts +44 -0
- package/dist/core/modules/better-auth/better-auth-models.js +185 -0
- package/dist/core/modules/better-auth/better-auth-models.js.map +1 -0
- package/dist/core/modules/better-auth/better-auth-rate-limit.middleware.d.ts +12 -0
- package/dist/core/modules/better-auth/better-auth-rate-limit.middleware.js +70 -0
- package/dist/core/modules/better-auth/better-auth-rate-limit.middleware.js.map +1 -0
- package/dist/core/modules/better-auth/better-auth-rate-limiter.service.d.ts +32 -0
- package/dist/core/modules/better-auth/better-auth-rate-limiter.service.js +173 -0
- package/dist/core/modules/better-auth/better-auth-rate-limiter.service.js.map +1 -0
- package/dist/core/modules/better-auth/better-auth-user.mapper.d.ts +43 -0
- package/dist/core/modules/better-auth/better-auth-user.mapper.js +159 -0
- package/dist/core/modules/better-auth/better-auth-user.mapper.js.map +1 -0
- package/dist/core/modules/better-auth/better-auth.config.d.ts +9 -0
- package/dist/core/modules/better-auth/better-auth.config.js +251 -0
- package/dist/core/modules/better-auth/better-auth.config.js.map +1 -0
- package/dist/core/modules/better-auth/better-auth.middleware.d.ts +20 -0
- package/dist/core/modules/better-auth/better-auth.middleware.js +79 -0
- package/dist/core/modules/better-auth/better-auth.middleware.js.map +1 -0
- package/dist/core/modules/better-auth/better-auth.module.d.ts +30 -0
- package/dist/core/modules/better-auth/better-auth.module.js +265 -0
- package/dist/core/modules/better-auth/better-auth.module.js.map +1 -0
- package/dist/core/modules/better-auth/better-auth.resolver.d.ts +49 -0
- package/dist/core/modules/better-auth/better-auth.resolver.js +539 -0
- package/dist/core/modules/better-auth/better-auth.resolver.js.map +1 -0
- package/dist/core/modules/better-auth/better-auth.service.d.ts +38 -0
- package/dist/core/modules/better-auth/better-auth.service.js +151 -0
- package/dist/core/modules/better-auth/better-auth.service.js.map +1 -0
- package/dist/core/modules/better-auth/better-auth.types.d.ts +38 -0
- package/dist/core/modules/better-auth/better-auth.types.js +15 -0
- package/dist/core/modules/better-auth/better-auth.types.js.map +1 -0
- package/dist/core/modules/better-auth/index.d.ts +11 -0
- package/dist/core/modules/better-auth/index.js +28 -0
- package/dist/core/modules/better-auth/index.js.map +1 -0
- package/dist/core/modules/user/core-user.model.d.ts +2 -0
- package/dist/core/modules/user/core-user.model.js +21 -0
- package/dist/core/modules/user/core-user.model.js.map +1 -1
- package/dist/core.module.js +7 -0
- 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/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +9 -1
- package/src/config.env.ts +148 -1
- package/src/core/common/decorators/restricted.decorator.ts +2 -2
- package/src/core/common/helpers/input.helper.ts +2 -2
- package/src/core/common/interfaces/server-options.interface.ts +344 -20
- package/src/core/modules/auth/auth-guard-strategy.enum.ts +1 -0
- package/src/core/modules/auth/guards/auth.guard.ts +20 -6
- package/src/core/modules/better-auth/README.md +1096 -0
- package/src/core/modules/better-auth/better-auth-auth.model.ts +69 -0
- package/src/core/modules/better-auth/better-auth-models.ts +143 -0
- package/src/core/modules/better-auth/better-auth-rate-limit.middleware.ts +113 -0
- package/src/core/modules/better-auth/better-auth-rate-limiter.service.ts +326 -0
- package/src/core/modules/better-auth/better-auth-user.mapper.ts +269 -0
- package/src/core/modules/better-auth/better-auth.config.ts +483 -0
- package/src/core/modules/better-auth/better-auth.middleware.ts +111 -0
- package/src/core/modules/better-auth/better-auth.module.ts +433 -0
- package/src/core/modules/better-auth/better-auth.resolver.ts +678 -0
- package/src/core/modules/better-auth/better-auth.service.ts +323 -0
- package/src/core/modules/better-auth/better-auth.types.ts +75 -0
- package/src/core/modules/better-auth/index.ts +25 -0
- package/src/core/modules/user/core-user.model.ts +29 -0
- package/src/core.module.ts +12 -0
- package/src/index.ts +6 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
import { passkey } from '@better-auth/passkey';
|
|
2
|
+
import { Logger } from '@nestjs/common';
|
|
3
|
+
import { betterAuth, BetterAuthPlugin } from 'better-auth';
|
|
4
|
+
import { mongodbAdapter } from 'better-auth/adapters/mongodb';
|
|
5
|
+
import { jwt, twoFactor } from 'better-auth/plugins';
|
|
6
|
+
import * as crypto from 'crypto';
|
|
7
|
+
|
|
8
|
+
import { IBetterAuth } from '../../common/interfaces/server-options.interface';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Type for better-auth instance with plugins
|
|
12
|
+
*/
|
|
13
|
+
export type BetterAuthInstance = ReturnType<typeof betterAuth>;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generates a cryptographically secure random secret.
|
|
17
|
+
* Used as fallback when no BETTER_AUTH_SECRET is configured.
|
|
18
|
+
*
|
|
19
|
+
* NOTE: This secret is generated at server startup, meaning:
|
|
20
|
+
* - All existing sessions become invalid on server restart
|
|
21
|
+
* - This is acceptable for development environments
|
|
22
|
+
* - For production, ALWAYS set BETTER_AUTH_SECRET to maintain sessions across restarts
|
|
23
|
+
*/
|
|
24
|
+
function generateSecureSecret(): string {
|
|
25
|
+
return crypto.randomBytes(32).toString('base64');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Cached auto-generated secret for the current server instance.
|
|
30
|
+
* Generated once at module load to ensure consistency within a single run.
|
|
31
|
+
*/
|
|
32
|
+
let cachedAutoGeneratedSecret: null | string = null;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Options for creating a better-auth instance
|
|
36
|
+
*/
|
|
37
|
+
export interface CreateBetterAuthOptions {
|
|
38
|
+
/**
|
|
39
|
+
* Better-auth configuration from server options
|
|
40
|
+
*/
|
|
41
|
+
config: IBetterAuth;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* MongoDB database instance
|
|
45
|
+
* Note: Uses 'any' type to handle version incompatibilities between
|
|
46
|
+
* mongoose's bundled mongodb types and the project's mongodb package.
|
|
47
|
+
* At runtime, this is a mongodb.Db instance.
|
|
48
|
+
*/
|
|
49
|
+
db: any;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Fallback secrets to try if no betterAuth.secret is configured.
|
|
53
|
+
* The array is iterated and the first valid secret (≥32 chars) is used.
|
|
54
|
+
* If no valid secret is found, an auto-generated secret is used.
|
|
55
|
+
*
|
|
56
|
+
* Typical usage: Pass existing secrets from your config (e.g., jwt.secret)
|
|
57
|
+
* for backwards compatibility.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```typescript
|
|
61
|
+
* fallbackSecrets: [config.jwt?.secret, config.jwt?.refresh?.secret]
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
fallbackSecrets?: (string | undefined)[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Better-Auth field type definition
|
|
69
|
+
* Matches the DBFieldType from better-auth
|
|
70
|
+
*/
|
|
71
|
+
type BetterAuthFieldType = 'boolean' | 'date' | 'json' | 'number' | 'number[]' | 'string' | 'string[]';
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Social provider configuration for better-auth
|
|
75
|
+
*/
|
|
76
|
+
interface SocialProviderConfig {
|
|
77
|
+
clientId: string;
|
|
78
|
+
clientSecret: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* User field configuration type for Better-Auth
|
|
83
|
+
* Matches the DBFieldAttribute structure from better-auth
|
|
84
|
+
*/
|
|
85
|
+
interface UserFieldConfig {
|
|
86
|
+
defaultValue?: unknown;
|
|
87
|
+
fieldName?: string;
|
|
88
|
+
required?: boolean;
|
|
89
|
+
type: BetterAuthFieldType;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Validation result for configuration
|
|
94
|
+
*/
|
|
95
|
+
interface ValidationResult {
|
|
96
|
+
errors: string[];
|
|
97
|
+
valid: boolean;
|
|
98
|
+
warnings: string[];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Creates a better-auth instance based on configuration
|
|
103
|
+
*
|
|
104
|
+
* @param options - Configuration options including betterAuth config and MongoDB connection
|
|
105
|
+
* @returns Configured better-auth instance or null if not enabled
|
|
106
|
+
* @throws Error if configuration validation fails
|
|
107
|
+
*/
|
|
108
|
+
export function createBetterAuthInstance(options: CreateBetterAuthOptions): BetterAuthInstance | null {
|
|
109
|
+
const logger = new Logger('BetterAuthConfig');
|
|
110
|
+
const { config, db, fallbackSecrets } = options;
|
|
111
|
+
|
|
112
|
+
// Return null only if better-auth is explicitly disabled
|
|
113
|
+
// BetterAuth is enabled by default (zero-config)
|
|
114
|
+
if (config?.enabled === false) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Validate configuration (with fallback secrets for backwards compatibility)
|
|
119
|
+
const validation = validateConfig(config, fallbackSecrets);
|
|
120
|
+
|
|
121
|
+
// Log warnings
|
|
122
|
+
for (const warning of validation.warnings) {
|
|
123
|
+
logger.warn(warning);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Throw on validation errors
|
|
127
|
+
if (!validation.valid) {
|
|
128
|
+
throw new Error(`BetterAuth configuration invalid: ${validation.errors.join('; ')}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Build configuration components
|
|
132
|
+
const plugins = buildPlugins(config);
|
|
133
|
+
const socialProviders = buildSocialProviders(config);
|
|
134
|
+
const trustedOrigins = buildTrustedOrigins(config);
|
|
135
|
+
const additionalFields = buildUserFields(config);
|
|
136
|
+
|
|
137
|
+
// Build the base Better-Auth configuration
|
|
138
|
+
const betterAuthConfig = {
|
|
139
|
+
basePath: config.basePath || '/iam',
|
|
140
|
+
baseURL: config.baseUrl || 'http://localhost:3000',
|
|
141
|
+
database: mongodbAdapter(db),
|
|
142
|
+
plugins,
|
|
143
|
+
secret: config.secret,
|
|
144
|
+
socialProviders,
|
|
145
|
+
trustedOrigins,
|
|
146
|
+
user: {
|
|
147
|
+
additionalFields,
|
|
148
|
+
modelName: 'users',
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Merge with custom options passthrough
|
|
153
|
+
// This allows projects to configure any Better-Auth option not explicitly defined
|
|
154
|
+
const finalConfig = config.options ? { ...betterAuthConfig, ...config.options } : betterAuthConfig;
|
|
155
|
+
|
|
156
|
+
// Create and return the better-auth instance
|
|
157
|
+
// Type assertion needed for maximum flexibility - allows projects to use any Better-Auth option
|
|
158
|
+
|
|
159
|
+
return betterAuth(finalConfig as any);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Builds the plugins array based on configuration.
|
|
164
|
+
* Merges built-in plugins (jwt, twoFactor, passkey) with custom plugins from config.
|
|
165
|
+
*
|
|
166
|
+
* Plugins are enabled by default when their configuration block is present.
|
|
167
|
+
* Set `enabled: false` to explicitly disable a configured plugin.
|
|
168
|
+
*/
|
|
169
|
+
function buildPlugins(config: IBetterAuth): BetterAuthPlugin[] {
|
|
170
|
+
const plugins: BetterAuthPlugin[] = [];
|
|
171
|
+
|
|
172
|
+
// JWT Plugin for API client compatibility
|
|
173
|
+
// Enabled by default when jwt config is present, unless explicitly disabled
|
|
174
|
+
if (config.jwt && config.jwt.enabled !== false) {
|
|
175
|
+
plugins.push(
|
|
176
|
+
jwt({
|
|
177
|
+
jwt: {
|
|
178
|
+
expirationTime: config.jwt.expiresIn || '15m',
|
|
179
|
+
},
|
|
180
|
+
}),
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Two-Factor Authentication Plugin
|
|
185
|
+
// Enabled by default when twoFactor config is present, unless explicitly disabled
|
|
186
|
+
if (config.twoFactor && config.twoFactor.enabled !== false) {
|
|
187
|
+
plugins.push(
|
|
188
|
+
twoFactor({
|
|
189
|
+
issuer: config.twoFactor.appName || 'Nest Server',
|
|
190
|
+
}),
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Passkey/WebAuthn Plugin
|
|
195
|
+
// Enabled by default when passkey config is present, unless explicitly disabled
|
|
196
|
+
if (config.passkey && config.passkey.enabled !== false) {
|
|
197
|
+
plugins.push(
|
|
198
|
+
passkey({
|
|
199
|
+
origin: config.passkey.origin || 'http://localhost:3000',
|
|
200
|
+
rpID: config.passkey.rpId || 'localhost',
|
|
201
|
+
rpName: config.passkey.rpName || 'Nest Server',
|
|
202
|
+
}),
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Merge custom plugins from configuration
|
|
207
|
+
// This allows projects to add any Better-Auth plugin without modifying this package
|
|
208
|
+
if (config.plugins?.length) {
|
|
209
|
+
plugins.push(...(config.plugins as BetterAuthPlugin[]));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return plugins;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Builds the social providers configuration object dynamically.
|
|
217
|
+
* Iterates over all configured providers and includes those that are enabled
|
|
218
|
+
* with valid clientId and clientSecret.
|
|
219
|
+
*/
|
|
220
|
+
function buildSocialProviders(config: IBetterAuth): Record<string, SocialProviderConfig> {
|
|
221
|
+
const socialProvidersConfig: Record<string, SocialProviderConfig> = {};
|
|
222
|
+
|
|
223
|
+
// Iterate over all configured social providers dynamically
|
|
224
|
+
// A provider is enabled by default if it has credentials configured
|
|
225
|
+
// It can be explicitly disabled by setting enabled: false
|
|
226
|
+
if (config.socialProviders) {
|
|
227
|
+
for (const [name, provider] of Object.entries(config.socialProviders)) {
|
|
228
|
+
if (provider?.clientId && provider?.clientSecret && provider?.enabled !== false) {
|
|
229
|
+
socialProvidersConfig[name] = {
|
|
230
|
+
clientId: provider.clientId,
|
|
231
|
+
clientSecret: provider.clientSecret,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return socialProvidersConfig;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Builds the trusted origins array
|
|
242
|
+
*/
|
|
243
|
+
function buildTrustedOrigins(config: IBetterAuth): string[] {
|
|
244
|
+
if (config.trustedOrigins?.length) {
|
|
245
|
+
return config.trustedOrigins;
|
|
246
|
+
}
|
|
247
|
+
if (config.baseUrl) {
|
|
248
|
+
return [config.baseUrl];
|
|
249
|
+
}
|
|
250
|
+
return ['http://localhost:3000'];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Builds the user additional fields configuration.
|
|
255
|
+
* Merges core fields (firstName, lastName, etc.) with custom fields from config.
|
|
256
|
+
* Custom fields override core fields if they have the same key.
|
|
257
|
+
*/
|
|
258
|
+
function buildUserFields(config: IBetterAuth): Record<string, UserFieldConfig> {
|
|
259
|
+
// Core fields required for nest-server functionality
|
|
260
|
+
const coreFields: Record<string, UserFieldConfig> = {
|
|
261
|
+
firstName: {
|
|
262
|
+
defaultValue: null,
|
|
263
|
+
fieldName: 'firstName',
|
|
264
|
+
type: 'string',
|
|
265
|
+
},
|
|
266
|
+
iamId: {
|
|
267
|
+
defaultValue: null,
|
|
268
|
+
fieldName: 'iamId',
|
|
269
|
+
type: 'string',
|
|
270
|
+
},
|
|
271
|
+
lastName: {
|
|
272
|
+
defaultValue: null,
|
|
273
|
+
fieldName: 'lastName',
|
|
274
|
+
type: 'string',
|
|
275
|
+
},
|
|
276
|
+
roles: {
|
|
277
|
+
defaultValue: [],
|
|
278
|
+
fieldName: 'roles',
|
|
279
|
+
type: 'string[]',
|
|
280
|
+
},
|
|
281
|
+
twoFactorEnabled: {
|
|
282
|
+
defaultValue: false,
|
|
283
|
+
fieldName: 'twoFactorEnabled',
|
|
284
|
+
type: 'boolean',
|
|
285
|
+
},
|
|
286
|
+
verified: {
|
|
287
|
+
defaultValue: false,
|
|
288
|
+
fieldName: 'verified',
|
|
289
|
+
type: 'boolean',
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
// Merge with custom additional fields from configuration
|
|
294
|
+
// Custom fields can override core fields or add new ones
|
|
295
|
+
if (config.additionalUserFields) {
|
|
296
|
+
for (const [key, field] of Object.entries(config.additionalUserFields)) {
|
|
297
|
+
coreFields[key] = {
|
|
298
|
+
defaultValue: field.defaultValue,
|
|
299
|
+
fieldName: field.fieldName || key,
|
|
300
|
+
required: field.required,
|
|
301
|
+
type: field.type,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return coreFields;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Gets or generates the fallback secret for development.
|
|
311
|
+
* The secret is cached to ensure consistency during the server's lifetime.
|
|
312
|
+
*/
|
|
313
|
+
function getAutoGeneratedSecret(): string {
|
|
314
|
+
if (!cachedAutoGeneratedSecret) {
|
|
315
|
+
cachedAutoGeneratedSecret = generateSecureSecret();
|
|
316
|
+
}
|
|
317
|
+
return cachedAutoGeneratedSecret;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Checks if a secret has valid minimum length (32 characters)
|
|
322
|
+
*/
|
|
323
|
+
function isValidSecretLength(secret: string): boolean {
|
|
324
|
+
return secret && secret.length >= 32;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Validates a URL string
|
|
329
|
+
*/
|
|
330
|
+
function isValidUrl(url: string): boolean {
|
|
331
|
+
try {
|
|
332
|
+
new URL(url);
|
|
333
|
+
return true;
|
|
334
|
+
} catch {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Validates the better-auth configuration and applies fallback secret if needed.
|
|
341
|
+
* Mutates config.secret if fallback is applied.
|
|
342
|
+
*
|
|
343
|
+
* Secret resolution order:
|
|
344
|
+
* 1. betterAuth.secret (if configured and valid)
|
|
345
|
+
* 2. First valid secret from fallbackSecrets array (≥32 chars)
|
|
346
|
+
* 3. Auto-generated secure secret (with warning)
|
|
347
|
+
*
|
|
348
|
+
* @param config - Better-auth configuration
|
|
349
|
+
* @param fallbackSecrets - Optional array of fallback secrets to try
|
|
350
|
+
*/
|
|
351
|
+
function validateConfig(config: IBetterAuth, fallbackSecrets?: (string | undefined)[]): ValidationResult {
|
|
352
|
+
const errors: string[] = [];
|
|
353
|
+
const warnings: string[] = [];
|
|
354
|
+
|
|
355
|
+
// Track secret source for appropriate messaging
|
|
356
|
+
let secretSource: 'auto-generated' | 'explicit' | 'fallback' = 'explicit';
|
|
357
|
+
|
|
358
|
+
// Resolve secret with fallback chain
|
|
359
|
+
if (!config.secret || config.secret.trim() === '') {
|
|
360
|
+
// Try fallback secrets in order
|
|
361
|
+
const validFallback = fallbackSecrets?.find((secret) => secret && isValidSecretLength(secret));
|
|
362
|
+
|
|
363
|
+
if (validFallback) {
|
|
364
|
+
config.secret = validFallback;
|
|
365
|
+
secretSource = 'fallback';
|
|
366
|
+
} else {
|
|
367
|
+
// Last resort: auto-generate
|
|
368
|
+
config.secret = getAutoGeneratedSecret();
|
|
369
|
+
secretSource = 'auto-generated';
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Validate the resolved secret
|
|
374
|
+
const secretValidation = validateSecret(config.secret);
|
|
375
|
+
if (!secretValidation.valid) {
|
|
376
|
+
errors.push(secretValidation.message!);
|
|
377
|
+
} else if (secretValidation.message) {
|
|
378
|
+
warnings.push(secretValidation.message);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Log information about secret source
|
|
382
|
+
switch (secretSource) {
|
|
383
|
+
case 'auto-generated':
|
|
384
|
+
warnings.push('⚠️ BETTER_AUTH: No secret configured - using auto-generated secret.');
|
|
385
|
+
warnings.push('⚠️ CONSEQUENCE: All user sessions will be invalidated on server restart!');
|
|
386
|
+
warnings.push(
|
|
387
|
+
'💡 FOR PRODUCTION: Set betterAuth.secret in config or provide a valid fallback secret (min 32 chars).',
|
|
388
|
+
);
|
|
389
|
+
warnings.push("💡 Generate with: node -e \"console.log(require('crypto').randomBytes(32).toString('base64'))\"");
|
|
390
|
+
break;
|
|
391
|
+
case 'fallback':
|
|
392
|
+
warnings.push(
|
|
393
|
+
'💡 BETTER_AUTH: Using fallback secret (backwards compatible). Consider setting betterAuth.secret explicitly.',
|
|
394
|
+
);
|
|
395
|
+
break;
|
|
396
|
+
// 'explicit' - no warning needed, explicitly configured
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Validate baseUrl
|
|
400
|
+
if (config.baseUrl && !isValidUrl(config.baseUrl)) {
|
|
401
|
+
errors.push(`Invalid baseUrl format: ${config.baseUrl}`);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Validate trustedOrigins
|
|
405
|
+
if (config.trustedOrigins) {
|
|
406
|
+
for (const origin of config.trustedOrigins) {
|
|
407
|
+
if (!isValidUrl(origin)) {
|
|
408
|
+
errors.push(`Invalid trustedOrigin format: ${origin}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Validate passkey origin
|
|
414
|
+
if (config.passkey?.enabled && config.passkey.origin && !isValidUrl(config.passkey.origin)) {
|
|
415
|
+
errors.push(`Invalid passkey origin format: ${config.passkey.origin}`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Validate social providers dynamically
|
|
419
|
+
// Providers are enabled by default unless explicitly disabled (enabled: false)
|
|
420
|
+
// We only validate credentials for providers that are not explicitly disabled
|
|
421
|
+
if (config.socialProviders) {
|
|
422
|
+
for (const [name, provider] of Object.entries(config.socialProviders)) {
|
|
423
|
+
// Provider is considered active if: configured AND not explicitly disabled
|
|
424
|
+
if (provider && provider.enabled !== false) {
|
|
425
|
+
// Validate that credentials are provided for active providers
|
|
426
|
+
const hasClientId = !!provider.clientId;
|
|
427
|
+
const hasClientSecret = !!provider.clientSecret;
|
|
428
|
+
|
|
429
|
+
if (hasClientId && hasClientSecret) {
|
|
430
|
+
// Both credentials present - provider is fully configured
|
|
431
|
+
continue;
|
|
432
|
+
} else if (hasClientId || hasClientSecret) {
|
|
433
|
+
// Only one credential provided - this is an error
|
|
434
|
+
if (!hasClientId) {
|
|
435
|
+
errors.push(`Social provider '${name}' is missing clientId`);
|
|
436
|
+
}
|
|
437
|
+
if (!hasClientSecret) {
|
|
438
|
+
errors.push(`Social provider '${name}' is missing clientSecret`);
|
|
439
|
+
}
|
|
440
|
+
} else {
|
|
441
|
+
// No credentials provided but provider is configured and not disabled
|
|
442
|
+
// This is likely a configuration mistake - warn the user
|
|
443
|
+
warnings.push(
|
|
444
|
+
`Social provider '${name}' is configured but missing both clientId and clientSecret. ` +
|
|
445
|
+
`Set 'enabled: false' to disable it explicitly, or provide credentials to enable it.`,
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return {
|
|
453
|
+
errors,
|
|
454
|
+
valid: errors.length === 0,
|
|
455
|
+
warnings,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Validates the secret strength
|
|
461
|
+
* Requirements: min 32 chars, should contain mixed characters
|
|
462
|
+
*/
|
|
463
|
+
function validateSecret(secret: string): { message?: string; valid: boolean } {
|
|
464
|
+
if (!secret || secret.length < 32) {
|
|
465
|
+
return { message: 'Secret must be at least 32 characters long', valid: false };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Check for character diversity (at least 2 of: lowercase, uppercase, numbers, special)
|
|
469
|
+
const hasLowercase = /[a-z]/.test(secret);
|
|
470
|
+
const hasUppercase = /[A-Z]/.test(secret);
|
|
471
|
+
const hasNumbers = /[0-9]/.test(secret);
|
|
472
|
+
const hasSpecial = /[^a-zA-Z0-9]/.test(secret);
|
|
473
|
+
const diversityCount = [hasLowercase, hasUppercase, hasNumbers, hasSpecial].filter(Boolean).length;
|
|
474
|
+
|
|
475
|
+
if (diversityCount < 2) {
|
|
476
|
+
return {
|
|
477
|
+
message: 'Secret should contain at least 2 different character types (lowercase, uppercase, numbers, special)',
|
|
478
|
+
valid: true, // Warning only, not an error
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return { valid: true };
|
|
483
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Injectable, Logger, NestMiddleware } from '@nestjs/common';
|
|
2
|
+
import { NextFunction, Request, Response } from 'express';
|
|
3
|
+
|
|
4
|
+
import { BetterAuthSessionUser, BetterAuthUserMapper, MappedUser } from './better-auth-user.mapper';
|
|
5
|
+
import { BetterAuthService } from './better-auth.service';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Extended Express Request with Better-Auth session data
|
|
9
|
+
*/
|
|
10
|
+
export interface BetterAuthRequest extends Request {
|
|
11
|
+
betterAuthSession?: {
|
|
12
|
+
session: any;
|
|
13
|
+
user: BetterAuthSessionUser;
|
|
14
|
+
};
|
|
15
|
+
betterAuthUser?: BetterAuthSessionUser;
|
|
16
|
+
user?: MappedUser | Request['user'];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Middleware that processes Better-Auth sessions and maps users
|
|
21
|
+
*
|
|
22
|
+
* This middleware:
|
|
23
|
+
* 1. Checks if Better-Auth is enabled
|
|
24
|
+
* 2. Validates the session using Better-Auth's API
|
|
25
|
+
* 3. Maps the Better-Auth user to our User model with hasRole() capability
|
|
26
|
+
* 4. Attaches the mapped user to req.user for use with our security decorators
|
|
27
|
+
*
|
|
28
|
+
* IMPORTANT: This middleware runs BEFORE guards, so the user will be available
|
|
29
|
+
* for RolesGuard and other security checks.
|
|
30
|
+
*/
|
|
31
|
+
@Injectable()
|
|
32
|
+
export class BetterAuthMiddleware implements NestMiddleware {
|
|
33
|
+
private readonly logger = new Logger(BetterAuthMiddleware.name);
|
|
34
|
+
|
|
35
|
+
constructor(
|
|
36
|
+
private readonly betterAuthService: BetterAuthService,
|
|
37
|
+
private readonly userMapper: BetterAuthUserMapper,
|
|
38
|
+
) {}
|
|
39
|
+
|
|
40
|
+
async use(req: BetterAuthRequest, _res: Response, next: NextFunction) {
|
|
41
|
+
// Skip if Better-Auth is not enabled
|
|
42
|
+
if (!this.betterAuthService.isEnabled()) {
|
|
43
|
+
return next();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Skip if user is already set (e.g., by JWT auth)
|
|
47
|
+
if (req.user) {
|
|
48
|
+
return next();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
// Get session from Better-Auth
|
|
53
|
+
const session = await this.getSession(req);
|
|
54
|
+
|
|
55
|
+
if (session?.user) {
|
|
56
|
+
// Store the original Better-Auth session
|
|
57
|
+
req.betterAuthSession = session;
|
|
58
|
+
req.betterAuthUser = session.user;
|
|
59
|
+
|
|
60
|
+
// Map the Better-Auth user to our User model with hasRole()
|
|
61
|
+
const mappedUser = await this.userMapper.mapSessionUser(session.user);
|
|
62
|
+
|
|
63
|
+
if (mappedUser) {
|
|
64
|
+
// Attach the mapped user to the request
|
|
65
|
+
// This makes it compatible with @CurrentUser() and RolesGuard
|
|
66
|
+
req.user = mappedUser;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
// Don't block the request on auth errors
|
|
71
|
+
// The guards will handle unauthorized access
|
|
72
|
+
this.logger.debug(`Session validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
next();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Gets the session from Better-Auth
|
|
80
|
+
*/
|
|
81
|
+
private async getSession(req: Request): Promise<null | { session: any; user: BetterAuthSessionUser }> {
|
|
82
|
+
const api = this.betterAuthService.getApi();
|
|
83
|
+
if (!api) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
// Convert Express headers to the format Better-Auth expects
|
|
89
|
+
const headers = new Headers();
|
|
90
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
91
|
+
if (typeof value === 'string') {
|
|
92
|
+
headers.set(key, value);
|
|
93
|
+
} else if (Array.isArray(value)) {
|
|
94
|
+
headers.set(key, value.join(', '));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Call Better-Auth's getSession API
|
|
99
|
+
const response = await api.getSession({ headers });
|
|
100
|
+
|
|
101
|
+
if (response && typeof response === 'object' && 'user' in response) {
|
|
102
|
+
return response as { session: any; user: BetterAuthSessionUser };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return null;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
this.logger.debug(`getSession error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|