@lenne.tech/nest-server 11.11.1 → 11.13.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/config.env.js +1 -0
- package/dist/config.env.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +16 -0
- 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.d.ts +13 -0
- package/dist/core/modules/better-auth/better-auth.config.js +114 -17
- package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.resolver.d.ts +7 -3
- package/dist/core/modules/better-auth/better-auth.resolver.js +16 -6
- package/dist/core/modules/better-auth/better-auth.resolver.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-api.middleware.d.ts +4 -2
- package/dist/core/modules/better-auth/core-better-auth-api.middleware.js +63 -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-auth.model.d.ts +1 -0
- package/dist/core/modules/better-auth/core-better-auth-auth.model.js +7 -0
- package/dist/core/modules/better-auth/core-better-auth-auth.model.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-email-verification.service.d.ts +48 -0
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js +241 -0
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js.map +1 -0
- package/dist/core/modules/better-auth/core-better-auth-models.d.ts +2 -1
- package/dist/core/modules/better-auth/core-better-auth-models.js +8 -4
- package/dist/core/modules/better-auth/core-better-auth-models.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-signup-validator.service.d.ts +18 -0
- package/dist/core/modules/better-auth/core-better-auth-signup-validator.service.js +82 -0
- package/dist/core/modules/better-auth/core-better-auth-signup-validator.service.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-user.mapper.d.ts +0 -1
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js +15 -8
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-web.helper.d.ts +3 -3
- package/dist/core/modules/better-auth/core-better-auth-web.helper.js +64 -44
- 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 +13 -1
- package/dist/core/modules/better-auth/core-better-auth.controller.js +108 -49
- 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 +57 -39
- package/dist/core/modules/better-auth/core-better-auth.middleware.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.module.d.ts +6 -0
- package/dist/core/modules/better-auth/core-better-auth.module.js +129 -24
- package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.resolver.d.ts +12 -5
- package/dist/core/modules/better-auth/core-better-auth.resolver.js +64 -17
- package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.service.d.ts +4 -1
- package/dist/core/modules/better-auth/core-better-auth.service.js +143 -23
- package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
- package/dist/core/modules/better-auth/index.d.ts +4 -0
- package/dist/core/modules/better-auth/index.js +4 -0
- package/dist/core/modules/better-auth/index.js.map +1 -1
- package/dist/core/modules/error-code/error-codes.d.ts +45 -0
- package/dist/core/modules/error-code/error-codes.js +40 -0
- package/dist/core/modules/error-code/error-codes.js.map +1 -1
- package/dist/core/modules/user/core-user.model.d.ts +1 -0
- package/dist/core/modules/user/core-user.model.js +11 -0
- package/dist/core/modules/user/core-user.model.js.map +1 -1
- package/dist/server/modules/better-auth/better-auth.controller.d.ts +3 -1
- package/dist/server/modules/better-auth/better-auth.controller.js +12 -3
- package/dist/server/modules/better-auth/better-auth.controller.js.map +1 -1
- package/dist/server/modules/better-auth/better-auth.resolver.d.ts +7 -3
- package/dist/server/modules/better-auth/better-auth.resolver.js +16 -6
- package/dist/server/modules/better-auth/better-auth.resolver.js.map +1 -1
- package/dist/server/modules/error-code/error-codes.d.ts +5 -0
- package/dist/server/modules/user/user.model.d.ts +5 -0
- package/dist/templates/email-verification-de.ejs +78 -0
- package/dist/templates/email-verification-en.ejs +78 -0
- package/dist/test/test.helper.d.ts +4 -0
- package/dist/test/test.helper.js +54 -1
- package/dist/test/test.helper.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/src/config.env.ts +2 -0
- package/src/core/common/interfaces/server-options.interface.ts +240 -0
- 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/INTEGRATION-CHECKLIST.md +113 -0
- package/src/core/modules/better-auth/README.md +72 -7
- package/src/core/modules/better-auth/better-auth-token.service.ts +5 -8
- package/src/core/modules/better-auth/better-auth.config.ts +282 -29
- package/src/core/modules/better-auth/better-auth.resolver.ts +16 -5
- package/src/core/modules/better-auth/core-better-auth-api.middleware.ts +100 -22
- package/src/core/modules/better-auth/core-better-auth-auth.model.ts +10 -0
- package/src/core/modules/better-auth/core-better-auth-cookie.helper.ts +323 -0
- package/src/core/modules/better-auth/core-better-auth-email-verification.service.ts +433 -0
- package/src/core/modules/better-auth/core-better-auth-models.ts +6 -3
- package/src/core/modules/better-auth/core-better-auth-signup-validator.service.ts +178 -0
- package/src/core/modules/better-auth/core-better-auth-token.helper.ts +200 -0
- package/src/core/modules/better-auth/core-better-auth-user.mapper.ts +18 -14
- package/src/core/modules/better-auth/core-better-auth-web.helper.ts +119 -69
- package/src/core/modules/better-auth/core-better-auth.controller.ts +197 -84
- package/src/core/modules/better-auth/core-better-auth.middleware.ts +93 -64
- package/src/core/modules/better-auth/core-better-auth.module.ts +215 -38
- package/src/core/modules/better-auth/core-better-auth.resolver.ts +140 -20
- package/src/core/modules/better-auth/core-better-auth.service.ts +210 -32
- package/src/core/modules/better-auth/index.ts +4 -0
- package/src/core/modules/error-code/error-codes.ts +45 -0
- package/src/core/modules/user/core-user.model.ts +15 -0
- package/src/server/modules/better-auth/better-auth.controller.ts +6 -2
- package/src/server/modules/better-auth/better-auth.resolver.ts +16 -5
- package/src/templates/email-verification-de.ejs +78 -0
- package/src/templates/email-verification-en.ejs +78 -0
- package/src/test/README.md +190 -0
- package/src/test/test.helper.ts +82 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BadRequestException, Logger, UnauthorizedException } from '@nestjs/common';
|
|
1
|
+
import { BadRequestException, Logger, Optional, UnauthorizedException } from '@nestjs/common';
|
|
2
2
|
import { Args, Context, Mutation, Query, Resolver } from '@nestjs/graphql';
|
|
3
3
|
import { Request, Response } from 'express';
|
|
4
4
|
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
requires2FA,
|
|
16
16
|
} from './better-auth.types';
|
|
17
17
|
import { CoreBetterAuthAuthModel } from './core-better-auth-auth.model';
|
|
18
|
+
import { CoreBetterAuthEmailVerificationService } from './core-better-auth-email-verification.service';
|
|
18
19
|
import { CoreBetterAuthMigrationStatusModel } from './core-better-auth-migration-status.model';
|
|
19
20
|
import {
|
|
20
21
|
CoreBetterAuth2FASetupModel,
|
|
@@ -24,7 +25,9 @@ import {
|
|
|
24
25
|
CoreBetterAuthSessionModel,
|
|
25
26
|
CoreBetterAuthUserModel,
|
|
26
27
|
} from './core-better-auth-models';
|
|
28
|
+
import { CoreBetterAuthSignUpValidatorService } from './core-better-auth-signup-validator.service';
|
|
27
29
|
import { BetterAuthSessionUser, CoreBetterAuthUserMapper, MappedUser } from './core-better-auth-user.mapper';
|
|
30
|
+
import { convertExpressHeaders } from './core-better-auth-web.helper';
|
|
28
31
|
import { CoreBetterAuthService } from './core-better-auth.service';
|
|
29
32
|
|
|
30
33
|
/**
|
|
@@ -69,8 +72,72 @@ export class CoreBetterAuthResolver {
|
|
|
69
72
|
constructor(
|
|
70
73
|
protected readonly betterAuthService: CoreBetterAuthService,
|
|
71
74
|
protected readonly userMapper: CoreBetterAuthUserMapper,
|
|
75
|
+
@Optional() protected readonly signUpValidator?: CoreBetterAuthSignUpValidatorService,
|
|
76
|
+
@Optional() protected readonly emailVerificationService?: CoreBetterAuthEmailVerificationService,
|
|
72
77
|
) {}
|
|
73
78
|
|
|
79
|
+
// ===========================================================================
|
|
80
|
+
// Token Resolution
|
|
81
|
+
// ===========================================================================
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Resolves a session token to a JWT when cookies are disabled and JWT is enabled.
|
|
85
|
+
* Delegates to CoreBetterAuthService.resolveJwtToken().
|
|
86
|
+
*
|
|
87
|
+
* @param token - The token from BetterAuth response (may be session token or JWT)
|
|
88
|
+
* @returns A proper JWT token, or the original token if conversion is not needed/possible
|
|
89
|
+
*/
|
|
90
|
+
protected async resolveJwtToken(token: string | undefined): Promise<string | undefined> {
|
|
91
|
+
return this.betterAuthService.resolveJwtToken(token);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ===========================================================================
|
|
95
|
+
// Email Verification
|
|
96
|
+
// ===========================================================================
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Check if email verification is required and the user's email is verified.
|
|
100
|
+
* Throws UnauthorizedException with EMAIL_VERIFICATION_REQUIRED if:
|
|
101
|
+
* - emailVerificationService is available AND enabled
|
|
102
|
+
* - AND the user's email is NOT verified
|
|
103
|
+
*
|
|
104
|
+
* Override this method to customize the email verification check behavior.
|
|
105
|
+
*
|
|
106
|
+
* @param sessionUser - The user from Better-Auth sign-in response
|
|
107
|
+
* @throws UnauthorizedException if email is not verified and verification is required
|
|
108
|
+
*/
|
|
109
|
+
protected checkEmailVerification(sessionUser: BetterAuthSessionUser): void {
|
|
110
|
+
if (
|
|
111
|
+
this.emailVerificationService?.isEnabled()
|
|
112
|
+
&& !sessionUser.emailVerified
|
|
113
|
+
) {
|
|
114
|
+
this.logger.debug(`[SignIn] Email not verified for ${maskEmail(sessionUser.email)}, blocking login`);
|
|
115
|
+
throw new UnauthorizedException(ErrorCode.EMAIL_VERIFICATION_REQUIRED);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Check email verification by looking up the user by email address.
|
|
121
|
+
*
|
|
122
|
+
* This is used in the 2FA path where the sign-in response does NOT include user data
|
|
123
|
+
* (only a 2FA challenge), so we cannot use checkEmailVerification(sessionUser).
|
|
124
|
+
* Instead, we look up the user via CoreBetterAuthService.isUserEmailVerified().
|
|
125
|
+
*
|
|
126
|
+
* @param email - The email address to check
|
|
127
|
+
* @throws UnauthorizedException if email is not verified and verification is required
|
|
128
|
+
*/
|
|
129
|
+
protected async checkEmailVerificationByEmail(email: string): Promise<void> {
|
|
130
|
+
if (!this.emailVerificationService?.isEnabled()) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const verified = await this.betterAuthService.isUserEmailVerified(email);
|
|
135
|
+
if (verified === false) {
|
|
136
|
+
this.logger.debug(`[SignIn/2FA] Email not verified for ${maskEmail(email)}, blocking login`);
|
|
137
|
+
throw new UnauthorizedException(ErrorCode.EMAIL_VERIFICATION_REQUIRED);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
74
141
|
// ===========================================================================
|
|
75
142
|
// Queries
|
|
76
143
|
// ===========================================================================
|
|
@@ -122,6 +189,7 @@ export class CoreBetterAuthResolver {
|
|
|
122
189
|
@Roles(RoleEnum.S_EVERYONE)
|
|
123
190
|
betterAuthFeatures(): CoreBetterAuthFeaturesModel {
|
|
124
191
|
return {
|
|
192
|
+
emailVerification: this.emailVerificationService?.isEnabled() ?? false,
|
|
125
193
|
enabled: this.betterAuthService.isEnabled(),
|
|
126
194
|
jwt: this.betterAuthService.isJwtEnabled(),
|
|
127
195
|
passkey: this.betterAuthService.isPasskeyEnabled(),
|
|
@@ -189,6 +257,11 @@ export class CoreBetterAuthResolver {
|
|
|
189
257
|
* but not in IAM, they will be automatically migrated on first IAM sign-in.
|
|
190
258
|
*
|
|
191
259
|
* Override this method to add custom pre/post sign-in logic.
|
|
260
|
+
*
|
|
261
|
+
* NOTE: The `_ctx` parameter is intentionally kept but unused in the base implementation.
|
|
262
|
+
* It provides override flexibility for consuming projects that need access to the Express
|
|
263
|
+
* Request/Response context (e.g., for IP logging, custom cookie handling, audit trails).
|
|
264
|
+
* DO NOT REMOVE this parameter - it would break existing project overrides.
|
|
192
265
|
*/
|
|
193
266
|
@Mutation(() => CoreBetterAuthAuthModel, {
|
|
194
267
|
description: 'Sign in via Better-Auth (email/password)',
|
|
@@ -197,8 +270,8 @@ export class CoreBetterAuthResolver {
|
|
|
197
270
|
async betterAuthSignIn(
|
|
198
271
|
@Args('email') email: string,
|
|
199
272
|
@Args('password') password: string,
|
|
200
|
-
|
|
201
|
-
@Context() _ctx
|
|
273
|
+
// Kept for override flexibility - projects may need req/res in their overrides
|
|
274
|
+
@Context() _ctx?: { req: Request; res: Response },
|
|
202
275
|
): Promise<CoreBetterAuthAuthModel> {
|
|
203
276
|
this.ensureEnabled();
|
|
204
277
|
|
|
@@ -245,6 +318,8 @@ export class CoreBetterAuthResolver {
|
|
|
245
318
|
|
|
246
319
|
// Check for 2FA requirement
|
|
247
320
|
if (requires2FA(response)) {
|
|
321
|
+
// Defense-in-depth: Check email verification even for 2FA users
|
|
322
|
+
await this.checkEmailVerificationByEmail(email);
|
|
248
323
|
return {
|
|
249
324
|
requiresTwoFactor: true,
|
|
250
325
|
success: false,
|
|
@@ -255,13 +330,19 @@ export class CoreBetterAuthResolver {
|
|
|
255
330
|
// Get user data
|
|
256
331
|
if (hasUser(response)) {
|
|
257
332
|
const sessionUser: BetterAuthSessionUser = response.user;
|
|
333
|
+
|
|
334
|
+
// Check email verification requirement before allowing login
|
|
335
|
+
this.checkEmailVerification(sessionUser);
|
|
336
|
+
|
|
258
337
|
const mappedUser = await this.userMapper.mapSessionUser(sessionUser);
|
|
259
338
|
|
|
260
|
-
// Return the
|
|
261
|
-
//
|
|
262
|
-
//
|
|
339
|
+
// Return the best available token:
|
|
340
|
+
// 1. accessToken (JWT plugin enriched response)
|
|
341
|
+
// 2. token (top-level, some BetterAuth versions)
|
|
342
|
+
// 3. session.token (session-based fallback)
|
|
263
343
|
const responseAny = response as any;
|
|
264
|
-
const
|
|
344
|
+
const rawToken = responseAny.accessToken || responseAny.token || (hasSession(response) ? response.session.token : undefined);
|
|
345
|
+
const token = await this.resolveJwtToken(rawToken);
|
|
265
346
|
|
|
266
347
|
return {
|
|
267
348
|
requiresTwoFactor: false,
|
|
@@ -299,7 +380,7 @@ export class CoreBetterAuthResolver {
|
|
|
299
380
|
/**
|
|
300
381
|
* Direct sign-in attempt without migration logic (used after migration)
|
|
301
382
|
*/
|
|
302
|
-
|
|
383
|
+
protected async attemptSignInDirect(
|
|
303
384
|
email: string,
|
|
304
385
|
password: string,
|
|
305
386
|
api: ReturnType<CoreBetterAuthService['getApi']>,
|
|
@@ -313,14 +394,24 @@ export class CoreBetterAuthResolver {
|
|
|
313
394
|
}
|
|
314
395
|
|
|
315
396
|
if (requires2FA(response)) {
|
|
397
|
+
// Defense-in-depth: Check email verification even for 2FA users
|
|
398
|
+
await this.checkEmailVerificationByEmail(email);
|
|
316
399
|
return { requiresTwoFactor: true, success: false, user: null };
|
|
317
400
|
}
|
|
318
401
|
|
|
319
402
|
const sessionUser: BetterAuthSessionUser = response.user;
|
|
403
|
+
|
|
404
|
+
// Check email verification requirement before allowing login
|
|
405
|
+
this.checkEmailVerification(sessionUser);
|
|
406
|
+
|
|
320
407
|
const mappedUser = await this.userMapper.mapSessionUser(sessionUser);
|
|
321
|
-
// Return
|
|
408
|
+
// Return the best available token:
|
|
409
|
+
// 1. accessToken (JWT plugin enriched response)
|
|
410
|
+
// 2. token (top-level, some BetterAuth versions)
|
|
411
|
+
// 3. session.token (session-based fallback)
|
|
322
412
|
const responseAny = response as any;
|
|
323
|
-
const
|
|
413
|
+
const rawToken = responseAny.accessToken || responseAny.token || (hasSession(response) ? response.session.token : undefined);
|
|
414
|
+
const token = await this.resolveJwtToken(rawToken);
|
|
324
415
|
|
|
325
416
|
return {
|
|
326
417
|
requiresTwoFactor: false,
|
|
@@ -335,6 +426,16 @@ export class CoreBetterAuthResolver {
|
|
|
335
426
|
* Sign up via Better-Auth
|
|
336
427
|
*
|
|
337
428
|
* Override this method to add custom pre/post sign-up logic (e.g., sending welcome emails).
|
|
429
|
+
*
|
|
430
|
+
* By default, `termsAndPrivacyAccepted` must be `true` for sign-up to succeed.
|
|
431
|
+
* This can be configured via `betterAuth.signUpChecks` in your server config:
|
|
432
|
+
* - `signUpChecks: false` - Disable all sign-up checks
|
|
433
|
+
* - `signUpChecks: { requiredFields: ['termsAndPrivacyAccepted', 'ageConfirmed'] }` - Custom fields
|
|
434
|
+
*
|
|
435
|
+
* @param email - User email address
|
|
436
|
+
* @param password - User password
|
|
437
|
+
* @param name - Optional display name
|
|
438
|
+
* @param termsAndPrivacyAccepted - Whether user accepted terms and privacy policy (required by default)
|
|
338
439
|
*/
|
|
339
440
|
@Mutation(() => CoreBetterAuthAuthModel, {
|
|
340
441
|
description: 'Sign up via Better-Auth (email/password)',
|
|
@@ -344,9 +445,15 @@ export class CoreBetterAuthResolver {
|
|
|
344
445
|
@Args('email') email: string,
|
|
345
446
|
@Args('password') password: string,
|
|
346
447
|
@Args('name', { nullable: true }) name?: string,
|
|
448
|
+
@Args('termsAndPrivacyAccepted', { nullable: true }) termsAndPrivacyAccepted?: boolean,
|
|
347
449
|
): Promise<CoreBetterAuthAuthModel> {
|
|
348
450
|
this.ensureEnabled();
|
|
349
451
|
|
|
452
|
+
// Validate sign-up input (termsAndPrivacyAccepted is required by default)
|
|
453
|
+
if (this.signUpValidator) {
|
|
454
|
+
this.signUpValidator.validateSignUpInput({ termsAndPrivacyAccepted });
|
|
455
|
+
}
|
|
456
|
+
|
|
350
457
|
const api = this.betterAuthService.getApi();
|
|
351
458
|
if (!api) {
|
|
352
459
|
throw new BadRequestException(ErrorCode.BETTERAUTH_API_NOT_AVAILABLE);
|
|
@@ -369,9 +476,26 @@ export class CoreBetterAuthResolver {
|
|
|
369
476
|
const sessionUser: BetterAuthSessionUser = response.user;
|
|
370
477
|
|
|
371
478
|
// Link or create user in our database
|
|
372
|
-
|
|
479
|
+
// Pass termsAndPrivacyAccepted to store the acceptance timestamp
|
|
480
|
+
await this.userMapper.linkOrCreateUser(sessionUser, { termsAndPrivacyAccepted });
|
|
373
481
|
const mappedUser = await this.userMapper.mapSessionUser(sessionUser);
|
|
374
482
|
|
|
483
|
+
// If email verification is enabled, revoke the session and don't return session data
|
|
484
|
+
// The user must verify their email before they can use any session
|
|
485
|
+
if (this.emailVerificationService?.isEnabled()) {
|
|
486
|
+
const sessionToken = hasSession(response) ? response.session.token : undefined;
|
|
487
|
+
if (sessionToken) {
|
|
488
|
+
await this.betterAuthService.revokeSession(sessionToken);
|
|
489
|
+
}
|
|
490
|
+
this.logger.debug(`[SignUp] Email verification required for ${maskEmail(sessionUser.email)}, session revoked`);
|
|
491
|
+
return {
|
|
492
|
+
emailVerificationRequired: true,
|
|
493
|
+
requiresTwoFactor: false,
|
|
494
|
+
success: true,
|
|
495
|
+
user: mappedUser ? this.mapToUserModel(mappedUser) : null,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
375
499
|
return {
|
|
376
500
|
requiresTwoFactor: false,
|
|
377
501
|
session: hasSession(response) ? this.mapSessionInfo(response.session) : null,
|
|
@@ -387,6 +511,10 @@ export class CoreBetterAuthResolver {
|
|
|
387
511
|
if (errorMessage.includes('already exists')) {
|
|
388
512
|
throw new BadRequestException(ErrorCode.EMAIL_ALREADY_EXISTS);
|
|
389
513
|
}
|
|
514
|
+
// Re-throw BadRequestException (e.g., from sign-up validation)
|
|
515
|
+
if (error instanceof BadRequestException) {
|
|
516
|
+
throw error;
|
|
517
|
+
}
|
|
390
518
|
throw new BadRequestException(ErrorCode.SIGNUP_FAILED);
|
|
391
519
|
}
|
|
392
520
|
}
|
|
@@ -782,15 +910,7 @@ export class CoreBetterAuthResolver {
|
|
|
782
910
|
* Convert Express headers to Web API Headers
|
|
783
911
|
*/
|
|
784
912
|
protected convertHeaders(headers: Record<string, string | string[] | undefined>): Headers {
|
|
785
|
-
|
|
786
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
787
|
-
if (typeof value === 'string') {
|
|
788
|
-
result.set(key, value);
|
|
789
|
-
} else if (Array.isArray(value)) {
|
|
790
|
-
result.set(key, value.join(', '));
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
return result;
|
|
913
|
+
return convertExpressHeaders(headers);
|
|
794
914
|
}
|
|
795
915
|
|
|
796
916
|
/**
|
|
@@ -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 { convertExpressHeaders, parseCookieHeader, signCookieValueIfNeeded } from './core-better-auth-web.helper';
|
|
12
13
|
import { BETTER_AUTH_INSTANCE } from './core-better-auth.module';
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -215,6 +216,48 @@ export class CoreBetterAuthService {
|
|
|
215
216
|
// JWT Token Methods
|
|
216
217
|
// ===================================================================================================================
|
|
217
218
|
|
|
219
|
+
/**
|
|
220
|
+
* Resolves a session token to a JWT when cookies are disabled and JWT is enabled.
|
|
221
|
+
*
|
|
222
|
+
* When cookies are disabled, BetterAuth's internal API may return a session token
|
|
223
|
+
* (opaque string like "TVRRtiL19h9q...") instead of a JWT. This method detects
|
|
224
|
+
* non-JWT tokens and converts them to proper JWTs via the BetterAuth JWT plugin.
|
|
225
|
+
*
|
|
226
|
+
* @param token - The token from BetterAuth response (may be session token or JWT)
|
|
227
|
+
* @returns A proper JWT token, or the original token if conversion is not needed/possible
|
|
228
|
+
*/
|
|
229
|
+
async resolveJwtToken(token: string | undefined): Promise<string | undefined> {
|
|
230
|
+
if (!token) return undefined;
|
|
231
|
+
|
|
232
|
+
const cookiesEnabled = ConfigService.configFastButReadOnly?.cookies !== false;
|
|
233
|
+
if (cookiesEnabled || !this.isJwtEnabled()) {
|
|
234
|
+
return token;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Already a JWT (three base64url segments separated by dots)
|
|
238
|
+
if (token.startsWith('eyJ') && token.split('.').length === 3) {
|
|
239
|
+
return token;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Convert session token to JWT via BetterAuth JWT plugin
|
|
243
|
+
// BetterAuth identifies sessions via the session cookie, so we pass
|
|
244
|
+
// the session token as a cookie header (signed, as BetterAuth expects)
|
|
245
|
+
const cookieName = this.getSessionCookieName();
|
|
246
|
+
const secret = this.config?.secret || '';
|
|
247
|
+
const signedToken = secret ? signCookieValueIfNeeded(token, secret, true) : encodeURIComponent(token);
|
|
248
|
+
const jwt = await this.getToken({
|
|
249
|
+
headers: { cookie: `${cookieName}=${signedToken}` },
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
if (jwt) {
|
|
253
|
+
this.logger.debug('Resolved session token to JWT for cookie-less mode');
|
|
254
|
+
return jwt;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
this.logger.warn('Failed to resolve session token to JWT - returning session token as fallback');
|
|
258
|
+
return token;
|
|
259
|
+
}
|
|
260
|
+
|
|
218
261
|
/**
|
|
219
262
|
* Gets a fresh JWT token for the current session.
|
|
220
263
|
*
|
|
@@ -248,16 +291,8 @@ export class CoreBetterAuthService {
|
|
|
248
291
|
|
|
249
292
|
try {
|
|
250
293
|
// Convert headers to the format Better-Auth expects
|
|
251
|
-
const headers = new Headers();
|
|
252
294
|
const reqHeaders = 'headers' in req ? req.headers : {};
|
|
253
|
-
|
|
254
|
-
for (const [key, value] of Object.entries(reqHeaders)) {
|
|
255
|
-
if (typeof value === 'string') {
|
|
256
|
-
headers.set(key, value);
|
|
257
|
-
} else if (Array.isArray(value)) {
|
|
258
|
-
headers.set(key, value.join(', '));
|
|
259
|
-
}
|
|
260
|
-
}
|
|
295
|
+
const headers = convertExpressHeaders(reqHeaders as Record<string, string | string[] | undefined>);
|
|
261
296
|
|
|
262
297
|
// Call the token endpoint via Better-Auth API
|
|
263
298
|
// The jwt plugin adds a getToken method to the API
|
|
@@ -305,26 +340,39 @@ export class CoreBetterAuthService {
|
|
|
305
340
|
|
|
306
341
|
try {
|
|
307
342
|
// Convert headers to the format Better-Auth expects
|
|
308
|
-
const headers = new Headers();
|
|
309
343
|
const reqHeaders = 'headers' in req ? req.headers : {};
|
|
344
|
+
const headers = convertExpressHeaders(reqHeaders as Record<string, string | string[] | undefined>);
|
|
310
345
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
346
|
+
// Sign cookies before sending to Better-Auth API
|
|
347
|
+
// Browser clients send unsigned cookies, but Better-Auth expects signed cookies
|
|
348
|
+
const cookieHeader = headers.get('cookie');
|
|
349
|
+
if (cookieHeader && this.config?.secret) {
|
|
350
|
+
const basePath = this.getBasePath()?.replace(/^\//, '').replace(/\//g, '.') || 'iam';
|
|
351
|
+
const sessionCookieName = `${basePath}.session_token`;
|
|
352
|
+
const cookies = parseCookieHeader(cookieHeader);
|
|
353
|
+
let modified = false;
|
|
354
|
+
|
|
355
|
+
for (const [name, value] of Object.entries(cookies)) {
|
|
356
|
+
// Sign the session token cookie if it's not already signed
|
|
357
|
+
if (name === sessionCookieName || name === 'token') {
|
|
358
|
+
const signedValue = signCookieValueIfNeeded(value, this.config.secret);
|
|
359
|
+
if (signedValue !== value) {
|
|
360
|
+
cookies[name] = signedValue;
|
|
361
|
+
modified = true;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
316
364
|
}
|
|
317
|
-
}
|
|
318
365
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
366
|
+
if (modified) {
|
|
367
|
+
const signedCookieHeader = Object.entries(cookies)
|
|
368
|
+
.map(([name, value]) => `${name}=${value}`)
|
|
369
|
+
.join('; ');
|
|
370
|
+
headers.set('cookie', signedCookieHeader);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
322
373
|
|
|
323
374
|
const response = await api.getSession({ headers });
|
|
324
375
|
|
|
325
|
-
// Debug: Log the response from api.getSession
|
|
326
|
-
this.logger.debug(`getSession response: ${JSON.stringify(response)?.substring(0, 200)}`);
|
|
327
|
-
|
|
328
376
|
if (response && typeof response === 'object' && 'user' in response) {
|
|
329
377
|
return response as SessionResult;
|
|
330
378
|
}
|
|
@@ -347,11 +395,11 @@ export class CoreBetterAuthService {
|
|
|
347
395
|
*
|
|
348
396
|
* @example
|
|
349
397
|
* ```typescript
|
|
350
|
-
* // Get session token from cookie
|
|
351
|
-
* const sessionToken = req.cookies['
|
|
398
|
+
* // Get session token from cookie (using basePath-based name)
|
|
399
|
+
* const sessionToken = req.cookies['iam.session_token'];
|
|
352
400
|
* const success = await betterAuthService.revokeSession(sessionToken);
|
|
353
401
|
* if (success) {
|
|
354
|
-
* res.clearCookie('
|
|
402
|
+
* res.clearCookie('iam.session_token');
|
|
355
403
|
* }
|
|
356
404
|
* ```
|
|
357
405
|
*/
|
|
@@ -521,6 +569,132 @@ export class CoreBetterAuthService {
|
|
|
521
569
|
}
|
|
522
570
|
}
|
|
523
571
|
|
|
572
|
+
/**
|
|
573
|
+
* Gets the most recent active (non-expired) session for a user.
|
|
574
|
+
*
|
|
575
|
+
* This is needed in JWT mode where the client only has a JWT but BetterAuth's
|
|
576
|
+
* plugin endpoints (2FA, Passkey, etc.) need a real session token for authentication.
|
|
577
|
+
*
|
|
578
|
+
* @param userId - The user ID to find an active session for
|
|
579
|
+
* @returns Session result with token, or null if no active session exists
|
|
580
|
+
*/
|
|
581
|
+
async getActiveSessionForUser(userId: string): Promise<SessionResult> {
|
|
582
|
+
if (!this.isEnabled() || !this.connection?.db) {
|
|
583
|
+
return { session: null, user: null };
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
try {
|
|
587
|
+
const db = this.connection.db;
|
|
588
|
+
const sessionsCollection = db.collection('session');
|
|
589
|
+
|
|
590
|
+
// Find the most recent non-expired session for this user
|
|
591
|
+
const results = await sessionsCollection
|
|
592
|
+
.aggregate([
|
|
593
|
+
{
|
|
594
|
+
$match: {
|
|
595
|
+
$expr: {
|
|
596
|
+
$or: [
|
|
597
|
+
{ $eq: ['$userId', userId] },
|
|
598
|
+
{ $eq: [{ $toString: '$userId' }, userId] },
|
|
599
|
+
],
|
|
600
|
+
},
|
|
601
|
+
expiresAt: { $gt: new Date() },
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
{ $sort: { createdAt: -1 } },
|
|
605
|
+
{ $limit: 1 },
|
|
606
|
+
{
|
|
607
|
+
$lookup: {
|
|
608
|
+
as: 'userDoc',
|
|
609
|
+
from: 'users',
|
|
610
|
+
let: { sessionUserId: '$userId' },
|
|
611
|
+
pipeline: [
|
|
612
|
+
{
|
|
613
|
+
$match: {
|
|
614
|
+
$expr: {
|
|
615
|
+
$or: [
|
|
616
|
+
{ $eq: ['$_id', '$$sessionUserId'] },
|
|
617
|
+
{ $eq: [{ $toString: '$_id' }, { $toString: '$$sessionUserId' }] },
|
|
618
|
+
{ $eq: ['$id', { $toString: '$$sessionUserId' }] },
|
|
619
|
+
],
|
|
620
|
+
},
|
|
621
|
+
},
|
|
622
|
+
},
|
|
623
|
+
],
|
|
624
|
+
},
|
|
625
|
+
},
|
|
626
|
+
{ $unwind: { path: '$userDoc', preserveNullAndEmptyArrays: true } },
|
|
627
|
+
])
|
|
628
|
+
.toArray();
|
|
629
|
+
|
|
630
|
+
const result = results[0];
|
|
631
|
+
|
|
632
|
+
if (!result) {
|
|
633
|
+
this.logger.debug(`getActiveSessionForUser: no active session for user ${userId}`);
|
|
634
|
+
return { session: null, user: null };
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const user = result.userDoc;
|
|
638
|
+
|
|
639
|
+
return {
|
|
640
|
+
session: {
|
|
641
|
+
expiresAt: result.expiresAt,
|
|
642
|
+
id: result.id || result._id?.toString(),
|
|
643
|
+
token: result.token,
|
|
644
|
+
userId: result.userId?.toString(),
|
|
645
|
+
},
|
|
646
|
+
user: user
|
|
647
|
+
? {
|
|
648
|
+
email: user.email,
|
|
649
|
+
emailVerified: user.emailVerified,
|
|
650
|
+
id: user.id || user._id?.toString(),
|
|
651
|
+
name: user.name,
|
|
652
|
+
}
|
|
653
|
+
: null,
|
|
654
|
+
};
|
|
655
|
+
} catch (error) {
|
|
656
|
+
this.logger.debug(`getActiveSessionForUser error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
657
|
+
return { session: null, user: null };
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// ===================================================================================================================
|
|
662
|
+
// User Lookup Methods
|
|
663
|
+
// ===================================================================================================================
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Checks whether a user's email is verified via Better-Auth's internal adapter.
|
|
667
|
+
*
|
|
668
|
+
* Returns:
|
|
669
|
+
* - `true` if the user exists and their email is verified
|
|
670
|
+
* - `false` if the user exists and their email is NOT verified
|
|
671
|
+
* - `null` if the user could not be found or Better-Auth is not available
|
|
672
|
+
*
|
|
673
|
+
* This method is used by controller and resolver to check email verification
|
|
674
|
+
* in the 2FA path, where the sign-in response does not include user data.
|
|
675
|
+
*
|
|
676
|
+
* @param email - The email address to look up
|
|
677
|
+
* @returns `true`, `false`, or `null`
|
|
678
|
+
*/
|
|
679
|
+
async isUserEmailVerified(email: string): Promise<boolean | null> {
|
|
680
|
+
const authInstance = this.getInstance();
|
|
681
|
+
if (!authInstance) {
|
|
682
|
+
return null;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
try {
|
|
686
|
+
const context = await authInstance.$context;
|
|
687
|
+
const result = await context.internalAdapter.findUserByEmail(email);
|
|
688
|
+
if (!result?.user) {
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
return !!result.user.emailVerified;
|
|
692
|
+
} catch (error) {
|
|
693
|
+
this.logger.debug(`isUserEmailVerified error for ${maskEmail(email)}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
524
698
|
// ===================================================================================================================
|
|
525
699
|
// JWT Token Verification (for BetterAuth JWT tokens using JWKS)
|
|
526
700
|
// ===================================================================================================================
|
|
@@ -686,22 +860,26 @@ export class CoreBetterAuthService {
|
|
|
686
860
|
}
|
|
687
861
|
|
|
688
862
|
/**
|
|
689
|
-
* Extracts and verifies a JWT token from a request's Authorization header
|
|
863
|
+
* Extracts and verifies a JWT token from a request's Authorization header,
|
|
864
|
+
* or verifies a directly provided token.
|
|
690
865
|
*
|
|
691
866
|
* @param req - Express request object
|
|
867
|
+
* @param token - Optional JWT token to verify directly (bypasses header extraction)
|
|
692
868
|
* @returns The JWT payload with user info, or null if no valid token
|
|
693
869
|
*/
|
|
694
|
-
async verifyJwtFromRequest(req: Request): Promise<null | {
|
|
870
|
+
async verifyJwtFromRequest(req: Request, token?: string): Promise<null | {
|
|
695
871
|
[key: string]: any;
|
|
696
872
|
email?: string;
|
|
697
873
|
sub: string;
|
|
698
874
|
}> {
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
875
|
+
if (!token) {
|
|
876
|
+
const authHeader = req.headers.authorization;
|
|
877
|
+
if (!authHeader?.startsWith('Bearer ')) {
|
|
878
|
+
return null;
|
|
879
|
+
}
|
|
880
|
+
token = authHeader.substring(7);
|
|
702
881
|
}
|
|
703
882
|
|
|
704
|
-
const token = authHeader.substring(7);
|
|
705
883
|
return this.verifyJwtToken(token);
|
|
706
884
|
}
|
|
707
885
|
}
|
|
@@ -28,10 +28,14 @@ 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';
|
|
32
|
+
export * from './core-better-auth-email-verification.service';
|
|
31
33
|
export * from './core-better-auth-migration-status.model';
|
|
32
34
|
export * from './core-better-auth-models';
|
|
33
35
|
export * from './core-better-auth-rate-limit.middleware';
|
|
34
36
|
export * from './core-better-auth-rate-limiter.service';
|
|
37
|
+
export * from './core-better-auth-signup-validator.service';
|
|
38
|
+
export * from './core-better-auth-token.helper';
|
|
35
39
|
export * from './core-better-auth-user.mapper';
|
|
36
40
|
export * from './core-better-auth-web.helper';
|
|
37
41
|
export * from './core-better-auth.controller';
|
|
@@ -233,6 +233,51 @@ export const LtnsErrors = {
|
|
|
233
233
|
},
|
|
234
234
|
},
|
|
235
235
|
|
|
236
|
+
SIGNUP_TERMS_NOT_ACCEPTED: {
|
|
237
|
+
code: 'LTNS_0021',
|
|
238
|
+
message: 'Terms and privacy policy must be accepted',
|
|
239
|
+
translations: {
|
|
240
|
+
de: 'Die Nutzungsbedingungen und Datenschutzrichtlinie müssen akzeptiert werden.',
|
|
241
|
+
en: 'Terms and privacy policy must be accepted.',
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
SIGNUP_MISSING_REQUIRED_FIELDS: {
|
|
246
|
+
code: 'LTNS_0022',
|
|
247
|
+
message: 'Required sign-up fields are missing',
|
|
248
|
+
translations: {
|
|
249
|
+
de: 'Erforderliche Registrierungsfelder fehlen.',
|
|
250
|
+
en: 'Required sign-up fields are missing.',
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
EMAIL_VERIFICATION_REQUIRED: {
|
|
255
|
+
code: 'LTNS_0023',
|
|
256
|
+
message: 'Email verification required',
|
|
257
|
+
translations: {
|
|
258
|
+
de: 'Bitte verifizieren Sie Ihre E-Mail-Adresse.',
|
|
259
|
+
en: 'Please verify your email address.',
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
EMAIL_VERIFICATION_TOKEN_INVALID: {
|
|
264
|
+
code: 'LTNS_0024',
|
|
265
|
+
message: 'Email verification token is invalid or expired',
|
|
266
|
+
translations: {
|
|
267
|
+
de: 'Der E-Mail-Verifizierungslink ist ungültig oder abgelaufen.',
|
|
268
|
+
en: 'The email verification link is invalid or expired.',
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
EMAIL_ALREADY_VERIFIED: {
|
|
273
|
+
code: 'LTNS_0025',
|
|
274
|
+
message: 'Email is already verified',
|
|
275
|
+
translations: {
|
|
276
|
+
de: 'Die E-Mail-Adresse ist bereits verifiziert.',
|
|
277
|
+
en: 'The email address is already verified.',
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
|
|
236
281
|
// =====================================================
|
|
237
282
|
// Authorization Errors (LTNS_0100-LTNS_0199)
|
|
238
283
|
// =====================================================
|
|
@@ -256,6 +256,21 @@ export abstract class CoreUserModel extends CorePersistenceModel {
|
|
|
256
256
|
})
|
|
257
257
|
twoFactorEnabled: boolean = undefined;
|
|
258
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Date when terms and privacy policy were accepted
|
|
261
|
+
* Set during sign-up when user accepts terms and privacy policy
|
|
262
|
+
*
|
|
263
|
+
* @since 11.13.0
|
|
264
|
+
*/
|
|
265
|
+
@UnifiedField({
|
|
266
|
+
description: 'Date when terms and privacy policy were accepted',
|
|
267
|
+
isOptional: true,
|
|
268
|
+
mongoose: { type: Date },
|
|
269
|
+
roles: RoleEnum.S_EVERYONE,
|
|
270
|
+
type: () => Date,
|
|
271
|
+
})
|
|
272
|
+
termsAndPrivacyAcceptedAt: Date = undefined;
|
|
273
|
+
|
|
259
274
|
// ===================================================================================================================
|
|
260
275
|
// Methods
|
|
261
276
|
// ===================================================================================================================
|