@lenne.tech/nest-server 11.6.2 → 11.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/dist/config.env.js +19 -12
  2. package/dist/config.env.js.map +1 -1
  3. package/dist/core/common/helpers/filter.helper.d.ts +9 -9
  4. package/dist/core/common/helpers/filter.helper.js +2 -4
  5. package/dist/core/common/helpers/filter.helper.js.map +1 -1
  6. package/dist/core/common/helpers/gridfs.helper.js +3 -3
  7. package/dist/core/common/helpers/gridfs.helper.js.map +1 -1
  8. package/dist/core/common/interfaces/server-options.interface.d.ts +21 -3
  9. package/dist/core/common/services/crud.service.d.ts +16 -16
  10. package/dist/core/common/services/crud.service.js +1 -1
  11. package/dist/core/common/services/crud.service.js.map +1 -1
  12. package/dist/core/modules/auth/core-auth.controller.d.ts +1 -0
  13. package/dist/core/modules/auth/core-auth.controller.js +28 -2
  14. package/dist/core/modules/auth/core-auth.controller.js.map +1 -1
  15. package/dist/core/modules/auth/core-auth.module.js +14 -1
  16. package/dist/core/modules/auth/core-auth.module.js.map +1 -1
  17. package/dist/core/modules/auth/core-auth.resolver.d.ts +1 -0
  18. package/dist/core/modules/auth/core-auth.resolver.js +20 -2
  19. package/dist/core/modules/auth/core-auth.resolver.js.map +1 -1
  20. package/dist/core/modules/auth/exceptions/legacy-auth-disabled.exception.d.ts +4 -0
  21. package/dist/core/modules/auth/exceptions/legacy-auth-disabled.exception.js +17 -0
  22. package/dist/core/modules/auth/exceptions/legacy-auth-disabled.exception.js.map +1 -0
  23. package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.d.ts +9 -0
  24. package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.js +74 -0
  25. package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.js.map +1 -0
  26. package/dist/core/modules/auth/interfaces/auth-provider.interface.d.ts +7 -0
  27. package/dist/core/modules/auth/interfaces/auth-provider.interface.js +5 -0
  28. package/dist/core/modules/auth/interfaces/auth-provider.interface.js.map +1 -0
  29. package/dist/core/modules/auth/interfaces/core-auth-user.interface.d.ts +1 -0
  30. package/dist/core/modules/auth/services/core-auth.service.d.ts +10 -1
  31. package/dist/core/modules/auth/services/core-auth.service.js +141 -9
  32. package/dist/core/modules/auth/services/core-auth.service.js.map +1 -1
  33. package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.d.ts +31 -0
  34. package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js +153 -0
  35. package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js.map +1 -0
  36. package/dist/core/modules/better-auth/better-auth-migration-status.model.d.ts +10 -0
  37. package/dist/core/modules/better-auth/better-auth-migration-status.model.js +57 -0
  38. package/dist/core/modules/better-auth/better-auth-migration-status.model.js.map +1 -0
  39. package/dist/core/modules/better-auth/better-auth-models.d.ts +0 -1
  40. package/dist/core/modules/better-auth/better-auth-models.js +0 -4
  41. package/dist/core/modules/better-auth/better-auth-models.js.map +1 -1
  42. package/dist/core/modules/better-auth/better-auth-user.mapper.d.ts +33 -0
  43. package/dist/core/modules/better-auth/better-auth-user.mapper.js +443 -0
  44. package/dist/core/modules/better-auth/better-auth-user.mapper.js.map +1 -1
  45. package/dist/core/modules/better-auth/better-auth.config.js +3 -0
  46. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
  47. package/dist/core/modules/better-auth/better-auth.module.d.ts +10 -2
  48. package/dist/core/modules/better-auth/better-auth.module.js +40 -52
  49. package/dist/core/modules/better-auth/better-auth.module.js.map +1 -1
  50. package/dist/core/modules/better-auth/better-auth.resolver.d.ts +8 -12
  51. package/dist/core/modules/better-auth/better-auth.resolver.js +33 -351
  52. package/dist/core/modules/better-auth/better-auth.resolver.js.map +1 -1
  53. package/dist/core/modules/better-auth/better-auth.service.d.ts +0 -1
  54. package/dist/core/modules/better-auth/better-auth.service.js +0 -3
  55. package/dist/core/modules/better-auth/better-auth.service.js.map +1 -1
  56. package/dist/core/modules/better-auth/better-auth.types.d.ts +9 -8
  57. package/dist/core/modules/better-auth/better-auth.types.js +14 -3
  58. package/dist/core/modules/better-auth/better-auth.types.js.map +1 -1
  59. package/dist/core/modules/better-auth/core-better-auth.controller.d.ts +67 -0
  60. package/dist/core/modules/better-auth/core-better-auth.controller.js +504 -0
  61. package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -0
  62. package/dist/core/modules/better-auth/core-better-auth.resolver.d.ts +61 -0
  63. package/dist/core/modules/better-auth/core-better-auth.resolver.js +552 -0
  64. package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -0
  65. package/dist/core/modules/better-auth/index.d.ts +3 -0
  66. package/dist/core/modules/better-auth/index.js +3 -0
  67. package/dist/core/modules/better-auth/index.js.map +1 -1
  68. package/dist/core/modules/user/core-user.service.d.ts +7 -1
  69. package/dist/core/modules/user/core-user.service.js +57 -3
  70. package/dist/core/modules/user/core-user.service.js.map +1 -1
  71. package/dist/core/modules/user/interfaces/core-user-service-options.interface.d.ts +4 -0
  72. package/dist/core/modules/user/interfaces/core-user-service-options.interface.js +3 -0
  73. package/dist/core/modules/user/interfaces/core-user-service-options.interface.js.map +1 -0
  74. package/dist/core.module.d.ts +3 -0
  75. package/dist/core.module.js +132 -54
  76. package/dist/core.module.js.map +1 -1
  77. package/dist/index.d.ts +5 -0
  78. package/dist/index.js +5 -0
  79. package/dist/index.js.map +1 -1
  80. package/dist/server/modules/auth/auth.resolver.js +2 -0
  81. package/dist/server/modules/auth/auth.resolver.js.map +1 -1
  82. package/dist/server/modules/better-auth/better-auth.controller.d.ts +10 -0
  83. package/dist/server/modules/better-auth/better-auth.controller.js +36 -0
  84. package/dist/server/modules/better-auth/better-auth.controller.js.map +1 -0
  85. package/dist/server/modules/better-auth/better-auth.module.d.ts +9 -0
  86. package/dist/server/modules/better-auth/better-auth.module.js +44 -0
  87. package/dist/server/modules/better-auth/better-auth.module.js.map +1 -0
  88. package/dist/server/modules/better-auth/better-auth.resolver.d.ts +47 -0
  89. package/dist/server/modules/better-auth/better-auth.resolver.js +234 -0
  90. package/dist/server/modules/better-auth/better-auth.resolver.js.map +1 -0
  91. package/dist/server/modules/file/file-info.model.d.ts +71 -3
  92. package/dist/server/modules/user/user.model.d.ts +169 -3
  93. package/dist/server/modules/user/user.service.d.ts +3 -1
  94. package/dist/server/modules/user/user.service.js +7 -3
  95. package/dist/server/modules/user/user.service.js.map +1 -1
  96. package/dist/server/server.module.js +6 -1
  97. package/dist/server/server.module.js.map +1 -1
  98. package/dist/tsconfig.build.tsbuildinfo +1 -1
  99. package/package.json +20 -29
  100. package/src/config.env.ts +34 -13
  101. package/src/core/common/helpers/filter.helper.ts +15 -17
  102. package/src/core/common/helpers/gridfs.helper.ts +5 -5
  103. package/src/core/common/interfaces/server-options.interface.ts +222 -14
  104. package/src/core/common/services/crud.service.ts +22 -22
  105. package/src/core/modules/auth/core-auth.controller.ts +93 -5
  106. package/src/core/modules/auth/core-auth.module.ts +15 -1
  107. package/src/core/modules/auth/core-auth.resolver.ts +70 -2
  108. package/src/core/modules/auth/exceptions/legacy-auth-disabled.exception.ts +35 -0
  109. package/src/core/modules/auth/guards/legacy-auth-rate-limit.guard.ts +109 -0
  110. package/src/core/modules/auth/interfaces/auth-provider.interface.ts +86 -0
  111. package/src/core/modules/auth/interfaces/core-auth-user.interface.ts +6 -0
  112. package/src/core/modules/auth/services/core-auth.service.ts +245 -6
  113. package/src/core/modules/auth/services/legacy-auth-rate-limiter.service.ts +283 -0
  114. package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +254 -0
  115. package/src/core/modules/better-auth/README.md +698 -54
  116. package/src/core/modules/better-auth/better-auth-migration-status.model.ts +73 -0
  117. package/src/core/modules/better-auth/better-auth-models.ts +0 -3
  118. package/src/core/modules/better-auth/better-auth-user.mapper.ts +805 -0
  119. package/src/core/modules/better-auth/better-auth.config.ts +5 -0
  120. package/src/core/modules/better-auth/better-auth.module.ts +107 -66
  121. package/src/core/modules/better-auth/better-auth.resolver.ts +88 -553
  122. package/src/core/modules/better-auth/better-auth.service.ts +0 -9
  123. package/src/core/modules/better-auth/better-auth.types.ts +25 -10
  124. package/src/core/modules/better-auth/core-better-auth.controller.ts +646 -0
  125. package/src/core/modules/better-auth/core-better-auth.resolver.ts +730 -0
  126. package/src/core/modules/better-auth/index.ts +9 -1
  127. package/src/core/modules/user/core-user.service.ts +131 -4
  128. package/src/core/modules/user/interfaces/core-user-service-options.interface.ts +15 -0
  129. package/src/core.module.ts +257 -74
  130. package/src/index.ts +5 -0
  131. package/src/server/modules/auth/auth.resolver.ts +8 -0
  132. package/src/server/modules/better-auth/better-auth.controller.ts +41 -0
  133. package/src/server/modules/better-auth/better-auth.module.ts +88 -0
  134. package/src/server/modules/better-auth/better-auth.resolver.ts +210 -0
  135. package/src/server/modules/user/user.service.ts +4 -2
  136. package/src/server/server.module.ts +10 -1
@@ -0,0 +1,109 @@
1
+ import { CanActivate, ExecutionContext, HttpException, HttpStatus, Injectable } from '@nestjs/common';
2
+ import { GqlExecutionContext } from '@nestjs/graphql';
3
+
4
+ import { LegacyAuthRateLimiter } from '../services/legacy-auth-rate-limiter.service';
5
+
6
+ /**
7
+ * Guard for rate limiting Legacy Auth endpoints
8
+ *
9
+ * This guard applies rate limiting to protect against brute-force attacks.
10
+ * It works with both REST and GraphQL endpoints.
11
+ *
12
+ * Rate limiting must be enabled via configuration:
13
+ * ```typescript
14
+ * auth: {
15
+ * rateLimit: {
16
+ * enabled: true,
17
+ * max: 10,
18
+ * windowSeconds: 60,
19
+ * }
20
+ * }
21
+ * ```
22
+ *
23
+ * @since 11.7.x
24
+ */
25
+ @Injectable()
26
+ export class LegacyAuthRateLimitGuard implements CanActivate {
27
+ constructor(private readonly rateLimiter: LegacyAuthRateLimiter) {}
28
+
29
+ canActivate(context: ExecutionContext): boolean {
30
+ // If rate limiting is disabled, always allow
31
+ if (!this.rateLimiter.isEnabled()) {
32
+ return true;
33
+ }
34
+
35
+ const { endpoint, ip } = this.extractRequestInfo(context);
36
+ const result = this.rateLimiter.check(ip, endpoint);
37
+
38
+ if (!result.allowed) {
39
+ throw new HttpException(
40
+ {
41
+ error: 'Too Many Requests',
42
+ message: this.rateLimiter.getMessage(),
43
+ remaining: result.remaining,
44
+ retryAfter: result.resetIn,
45
+ statusCode: HttpStatus.TOO_MANY_REQUESTS,
46
+ },
47
+ HttpStatus.TOO_MANY_REQUESTS,
48
+ );
49
+ }
50
+
51
+ return true;
52
+ }
53
+
54
+ /**
55
+ * Extract IP and endpoint from the execution context
56
+ */
57
+ private extractRequestInfo(context: ExecutionContext): { endpoint: string; ip: string } {
58
+ const contextType = context.getType<'graphql' | 'http'>();
59
+
60
+ if (contextType === 'graphql') {
61
+ const gqlContext = GqlExecutionContext.create(context);
62
+ const info = gqlContext.getInfo();
63
+ const ctx = gqlContext.getContext();
64
+
65
+ // Get IP from request
66
+ const req = ctx.req;
67
+ const ip = this.getClientIp(req);
68
+
69
+ // Get endpoint from GraphQL field name
70
+ const endpoint = info?.fieldName || 'unknown';
71
+
72
+ return { endpoint, ip };
73
+ }
74
+
75
+ // HTTP context
76
+ const request = context.switchToHttp().getRequest();
77
+ const ip = this.getClientIp(request);
78
+
79
+ // Get endpoint from URL path
80
+ const url = request.url || request.path || '';
81
+ const endpoint = url.split('/').pop() || 'unknown';
82
+
83
+ return { endpoint, ip };
84
+ }
85
+
86
+ /**
87
+ * Get client IP from request, handling proxies
88
+ */
89
+ private getClientIp(request: any): string {
90
+ if (!request) {
91
+ return 'unknown';
92
+ }
93
+
94
+ // Check common proxy headers
95
+ const forwardedFor = request.headers?.['x-forwarded-for'];
96
+ if (forwardedFor) {
97
+ // Take the first IP in the chain (original client)
98
+ return forwardedFor.split(',')[0].trim();
99
+ }
100
+
101
+ const realIp = request.headers?.['x-real-ip'];
102
+ if (realIp) {
103
+ return realIp;
104
+ }
105
+
106
+ // Fall back to direct connection IP
107
+ return request.ip || request.connection?.remoteAddress || 'unknown';
108
+ }
109
+ }
@@ -0,0 +1,86 @@
1
+ import { JwtPayload } from './jwt-payload.interface';
2
+
3
+ /**
4
+ * Auth Provider Interface
5
+ *
6
+ * This interface defines the contract for authentication providers.
7
+ * Both Legacy Auth (CoreAuthService) and BetterAuth can implement this interface,
8
+ * allowing CoreModule to work with either system transparently.
9
+ *
10
+ * @since 11.8.0
11
+ * @see https://github.com/lenneTech/nest-server/blob/develop/src/core/modules/auth/interfaces/auth-provider.interface.ts
12
+ *
13
+ * ## Roadmap
14
+ *
15
+ * ### v11.x (Current)
16
+ * - Interface introduced for future flexibility
17
+ * - Legacy Auth (CoreAuthService) is the default implementation
18
+ * - BetterAuth can be used alongside Legacy Auth
19
+ *
20
+ * ### Future Version (Planned)
21
+ * - CoreModule.forRoot will use IAuthProvider instead of concrete AuthService
22
+ * - Legacy Auth becomes optional (must be explicitly enabled)
23
+ * - BetterAuth becomes the recommended default
24
+ *
25
+ * ## Implementation Example
26
+ *
27
+ * ```typescript
28
+ * @Injectable()
29
+ * export class BetterAuthProvider implements IAuthProvider {
30
+ * constructor(private readonly betterAuthService: BetterAuthService) {}
31
+ *
32
+ * decodeJwt(token: string): JwtPayload {
33
+ * return this.betterAuthService.decodeJwt(token);
34
+ * }
35
+ *
36
+ * async validateUser(payload: JwtPayload): Promise<any> {
37
+ * return this.betterAuthService.validateUser(payload);
38
+ * }
39
+ *
40
+ * signToken(user: any, expiresIn?: string): string {
41
+ * return this.betterAuthService.signToken(user, expiresIn);
42
+ * }
43
+ * }
44
+ * ```
45
+ */
46
+ export interface IAuthProvider {
47
+ /**
48
+ * Decode a JWT token without verification
49
+ * Used for extracting payload information
50
+ *
51
+ * @param token - The JWT token to decode
52
+ * @returns The decoded JWT payload
53
+ */
54
+ decodeJwt(token: string): JwtPayload;
55
+
56
+ /**
57
+ * Sign a new JWT token for a user
58
+ *
59
+ * @param user - The user to create a token for
60
+ * @param expiresIn - Optional expiration time (e.g., '15m', '7d')
61
+ * @returns The signed JWT token
62
+ */
63
+ signToken(user: any, expiresIn?: string): string;
64
+
65
+ /**
66
+ * Validate a user based on JWT payload
67
+ * Called during authentication to verify the user exists and is valid
68
+ *
69
+ * @param payload - The JWT payload containing user information
70
+ * @returns The validated user object, or null if invalid
71
+ */
72
+ validateUser(payload: JwtPayload): Promise<any>;
73
+ }
74
+
75
+ /**
76
+ * Auth Provider Token for dependency injection
77
+ *
78
+ * Use this token to inject the auth provider in your services:
79
+ *
80
+ * ```typescript
81
+ * constructor(
82
+ * @Inject(AUTH_PROVIDER) private readonly authProvider: IAuthProvider,
83
+ * ) {}
84
+ * ```
85
+ */
86
+ export const AUTH_PROVIDER = 'AUTH_PROVIDER';
@@ -9,6 +9,12 @@ export interface ICoreAuthUser {
9
9
  */
10
10
  email: string;
11
11
 
12
+ /**
13
+ * Better-Auth (IAM) user ID for linking
14
+ * Set when user is migrated or created via IAM
15
+ */
16
+ iamId?: string;
17
+
12
18
  /**
13
19
  * ID of the user
14
20
  */
@@ -1,4 +1,11 @@
1
- import { BadRequestException, Injectable, UnauthorizedException } from '@nestjs/common';
1
+ import {
2
+ BadRequestException,
3
+ Injectable,
4
+ Logger,
5
+ NotFoundException,
6
+ Optional,
7
+ UnauthorizedException,
8
+ } from '@nestjs/common';
2
9
  import { JwtService } from '@nestjs/jwt';
3
10
  import bcrypt = require('bcrypt');
4
11
  import { randomUUID } from 'crypto';
@@ -8,6 +15,8 @@ import { getStringIds } from '../../../common/helpers/db.helper';
8
15
  import { prepareServiceOptions } from '../../../common/helpers/service.helper';
9
16
  import { ServiceOptions } from '../../../common/interfaces/service-options.interface';
10
17
  import { ConfigService } from '../../../common/services/config.service';
18
+ import { BetterAuthUserMapper } from '../../better-auth/better-auth-user.mapper';
19
+ import { BetterAuthService } from '../../better-auth/better-auth.service';
11
20
  import { CoreAuthModel } from '../core-auth.model';
12
21
  import { CoreAuthSignInInput } from '../inputs/core-auth-sign-in.input';
13
22
  import { CoreAuthSignUpInput } from '../inputs/core-auth-sign-up.input';
@@ -29,9 +38,23 @@ export interface GetResultOptions {
29
38
 
30
39
  /**
31
40
  * CoreAuthService to handle user authentication
41
+ *
42
+ * When Better-Auth (IAM) is enabled, this service delegates authentication to IAM
43
+ * while maintaining backwards compatibility by returning Legacy JWT format tokens.
44
+ *
45
+ * Migration strategy:
46
+ * - New users: Created directly in IAM with scrypt password hash
47
+ * - Existing Legacy users: Lazily migrated on first sign-in
48
+ * (Legacy bcrypt password verified, then IAM account created with scrypt hash)
49
+ *
50
+ * @deprecated The signIn and signUp methods are deprecated when IAM is enabled.
51
+ * Use the IAM REST endpoints (/iam/sign-in/email, /iam/sign-up/email) directly
52
+ * for new implementations. Legacy endpoints remain for backwards compatibility.
32
53
  */
33
54
  @Injectable()
34
55
  export class CoreAuthService {
56
+ private readonly logger = new Logger(CoreAuthService.name);
57
+
35
58
  /**
36
59
  * Integrate services
37
60
  */
@@ -39,6 +62,8 @@ export class CoreAuthService {
39
62
  protected readonly userService: CoreAuthUserService,
40
63
  protected readonly jwtService: JwtService,
41
64
  protected readonly configService: ConfigService,
65
+ @Optional() protected readonly betterAuthService?: BetterAuthService,
66
+ @Optional() protected readonly betterAuthUserMapper?: BetterAuthUserMapper,
42
67
  ) {}
43
68
 
44
69
  /**
@@ -99,6 +124,14 @@ export class CoreAuthService {
99
124
 
100
125
  /**
101
126
  * User sign in via email
127
+ *
128
+ * When IAM is enabled, this method:
129
+ * 1. For migrated users (have iamId): Verifies password via IAM
130
+ * 2. For non-migrated users: Verifies via Legacy, then migrates to IAM
131
+ *
132
+ * Always returns Legacy JWT format for backwards compatibility.
133
+ *
134
+ * @deprecated When IAM is enabled, prefer using /iam/sign-in/email REST endpoint directly.
102
135
  */
103
136
  async signIn(input: CoreAuthSignInInput, serviceOptions?: ServiceOptions): Promise<CoreAuthModel> {
104
137
  // Check input
@@ -106,6 +139,9 @@ export class CoreAuthService {
106
139
  throw new BadRequestException('Missing input');
107
140
  }
108
141
 
142
+ // Check if user enumeration prevention is enabled
143
+ const preventUserEnumeration = this.configService.getFastButReadOnly('auth.preventUserEnumeration', false);
144
+
109
145
  // Prepare service options
110
146
  const serviceOptionsForUserService = prepareServiceOptions(serviceOptions, {
111
147
  // We need password, so we can't use prepare output handling and have to deactivate it
@@ -119,12 +155,47 @@ export class CoreAuthService {
119
155
  const { deviceDescription, deviceId, email, password } = input;
120
156
 
121
157
  // Get user
122
- const user = await this.userService.getViaEmail(email, serviceOptionsForUserService);
158
+ let user: ICoreAuthUser;
159
+ try {
160
+ user = await this.userService.getViaEmail(email, serviceOptionsForUserService);
161
+ } catch (error) {
162
+ if (error instanceof NotFoundException) {
163
+ throw new UnauthorizedException(preventUserEnumeration ? 'Invalid credentials' : 'Unknown email');
164
+ }
165
+ throw error;
166
+ }
123
167
  if (!user) {
124
- throw new UnauthorizedException('Unknown email');
168
+ throw new UnauthorizedException(preventUserEnumeration ? 'Invalid credentials' : 'Unknown email');
125
169
  }
126
- if (!((await bcrypt.compare(password, user.password)) || (await bcrypt.compare(sha256(password), user.password)))) {
127
- throw new UnauthorizedException('Wrong password');
170
+
171
+ // Determine if IAM delegation is available
172
+ const iamEnabled = this.isIamEnabled();
173
+
174
+ if (iamEnabled && user.iamId) {
175
+ // User is already migrated to IAM - verify via IAM
176
+ const iamVerified = await this.verifyPasswordViaIam(email, password);
177
+ if (!iamVerified) {
178
+ throw new UnauthorizedException(preventUserEnumeration ? 'Invalid credentials' : 'Wrong password');
179
+ }
180
+ this.logger.debug(`User ${email} authenticated via IAM (already migrated)`);
181
+ } else {
182
+ // Verify via Legacy (bcrypt)
183
+ // Check if user has a password (social login only users don't have one)
184
+ if (!user.password) {
185
+ throw new UnauthorizedException(
186
+ preventUserEnumeration ? 'Invalid credentials' : 'No password set for this account',
187
+ );
188
+ }
189
+ if (
190
+ !((await bcrypt.compare(password, user.password)) || (await bcrypt.compare(sha256(password), user.password)))
191
+ ) {
192
+ throw new UnauthorizedException(preventUserEnumeration ? 'Invalid credentials' : 'Wrong password');
193
+ }
194
+
195
+ // If IAM is enabled but user not migrated, migrate them now
196
+ if (iamEnabled && !user.iamId) {
197
+ await this.migrateUserToIam(user, email, password);
198
+ }
128
199
  }
129
200
 
130
201
  // Return tokens and user with currentUser set so securityCheck knows user is requesting own data
@@ -136,6 +207,13 @@ export class CoreAuthService {
136
207
 
137
208
  /**
138
209
  * Register a new user account
210
+ *
211
+ * When IAM is enabled, this method:
212
+ * 1. Creates the user in IAM first (with scrypt password hash)
213
+ * 2. Creates/links the Legacy user with iamId
214
+ * 3. Returns Legacy JWT format for backwards compatibility
215
+ *
216
+ * @deprecated When IAM is enabled, prefer using /iam/sign-up/email REST endpoint directly.
139
217
  */
140
218
  async signUp(input: CoreAuthSignUpInput, serviceOptions?: ServiceOptions): Promise<CoreAuthModel> {
141
219
  // Prepare service options
@@ -146,7 +224,19 @@ export class CoreAuthService {
146
224
 
147
225
  // Get and check user
148
226
  try {
149
- const user = await this.userService.create(input, serviceOptionsForUserService);
227
+ // Determine if IAM delegation is available
228
+ const iamEnabled = this.isIamEnabled();
229
+
230
+ let user: ICoreAuthUser;
231
+
232
+ if (iamEnabled) {
233
+ // Create via IAM first, then create/link Legacy user
234
+ user = await this.createUserViaIam(input, serviceOptionsForUserService);
235
+ } else {
236
+ // Create via Legacy
237
+ user = await this.userService.create(input, serviceOptionsForUserService);
238
+ }
239
+
150
240
  if (!user) {
151
241
  throw new BadRequestException('Email address already in use');
152
242
  }
@@ -332,4 +422,153 @@ export class CoreAuthService {
332
422
  // Return new token
333
423
  return newRefreshToken;
334
424
  }
425
+
426
+ // ===================================================================================================================
427
+ // IAM Delegation Helper Methods
428
+ // ===================================================================================================================
429
+
430
+ /**
431
+ * Checks if IAM (Better-Auth) delegation is available and enabled
432
+ */
433
+ protected isIamEnabled(): boolean {
434
+ return !!(this.betterAuthService?.isEnabled() && this.betterAuthUserMapper);
435
+ }
436
+
437
+ /**
438
+ * Verifies password via IAM for already-migrated users
439
+ *
440
+ * @param email - User email
441
+ * @param password - Plain password to verify
442
+ * @returns true if password is valid, false otherwise
443
+ */
444
+ protected async verifyPasswordViaIam(email: string, password: string): Promise<boolean> {
445
+ if (!this.betterAuthService) {
446
+ return false;
447
+ }
448
+
449
+ const api = this.betterAuthService.getApi();
450
+ if (!api) {
451
+ return false;
452
+ }
453
+
454
+ try {
455
+ const response = await api.signInEmail({
456
+ body: { email, password },
457
+ });
458
+
459
+ // Check if response indicates successful authentication
460
+ return !!(response && 'user' in response && response.user);
461
+ } catch (error) {
462
+ this.logger.debug(
463
+ `IAM password verification failed for ${email}: ${error instanceof Error ? error.message : 'Unknown error'}`,
464
+ );
465
+ return false;
466
+ }
467
+ }
468
+
469
+ /**
470
+ * Migrates a Legacy user to IAM
471
+ *
472
+ * This creates the IAM user and account with a scrypt password hash,
473
+ * then links the Legacy user via iamId.
474
+ *
475
+ * @param user - The Legacy user to migrate
476
+ * @param email - User email
477
+ * @param plainPassword - Plain password (needed to create scrypt hash)
478
+ */
479
+ protected async migrateUserToIam(user: ICoreAuthUser, email: string, plainPassword: string): Promise<void> {
480
+ if (!this.betterAuthUserMapper) {
481
+ return;
482
+ }
483
+
484
+ try {
485
+ // Create IAM account with the plain password (creates scrypt hash)
486
+ const migrated = await this.betterAuthUserMapper.migrateAccountToIam(email, plainPassword);
487
+
488
+ if (migrated) {
489
+ this.logger.log(`Migrated Legacy user ${email} to IAM`);
490
+
491
+ // Refresh user to get updated iamId
492
+ const updatedUser = await this.userService.getViaEmail(email, { force: true });
493
+ if (updatedUser?.iamId) {
494
+ // Update the user object in place so subsequent operations see the iamId
495
+ (user as any).iamId = updatedUser.iamId;
496
+ }
497
+ }
498
+ } catch (error) {
499
+ // Log but don't throw - migration failure shouldn't block login
500
+ this.logger.warn(
501
+ `Failed to migrate user ${email} to IAM: ${error instanceof Error ? error.message : 'Unknown error'}`,
502
+ );
503
+ }
504
+ }
505
+
506
+ /**
507
+ * Creates a user via IAM and links to Legacy user
508
+ *
509
+ * @param input - Sign-up input data
510
+ * @param serviceOptions - Service options for user service
511
+ * @returns The created Legacy user (linked to IAM)
512
+ */
513
+ protected async createUserViaIam(input: CoreAuthSignUpInput, serviceOptions: ServiceOptions): Promise<ICoreAuthUser> {
514
+ if (!this.betterAuthService || !this.betterAuthUserMapper) {
515
+ throw new BadRequestException('IAM service not available');
516
+ }
517
+
518
+ const api = this.betterAuthService.getApi();
519
+ if (!api) {
520
+ throw new BadRequestException('IAM API not available');
521
+ }
522
+
523
+ try {
524
+ // Create user in IAM first
525
+ // Note: firstName and lastName are project-specific fields, may not exist on CoreAuthSignUpInput
526
+ const inputAny = input as any;
527
+ const name = [inputAny.firstName, inputAny.lastName].filter(Boolean).join(' ') || input.email.split('@')[0];
528
+ const response = await api.signUpEmail({
529
+ body: {
530
+ email: input.email,
531
+ name,
532
+ password: input.password,
533
+ },
534
+ });
535
+
536
+ if (!response || !('user' in response) || !response.user) {
537
+ throw new BadRequestException('Email address already in use');
538
+ }
539
+
540
+ // Link or create Legacy user with iamId
541
+ const iamUser = response.user as { email: string; id: string; name?: string };
542
+ const syncedUser = await this.betterAuthUserMapper.linkOrCreateUser(iamUser as any, {
543
+ firstName: inputAny.firstName,
544
+ lastName: inputAny.lastName,
545
+ });
546
+
547
+ if (!syncedUser) {
548
+ throw new BadRequestException('Failed to create user');
549
+ }
550
+
551
+ // Sync password to Legacy (enables backwards compatibility)
552
+ // Pass plain password so bcrypt hash can be created for Legacy Auth
553
+ await this.betterAuthUserMapper.syncPasswordToLegacy(iamUser.id, input.email, input.password);
554
+
555
+ this.logger.log(`Created user ${input.email} via IAM`);
556
+
557
+ // Get the full user from our database
558
+ const user = await this.userService.getViaEmail(input.email, serviceOptions);
559
+ if (!user) {
560
+ throw new BadRequestException('Failed to retrieve created user');
561
+ }
562
+
563
+ return user;
564
+ } catch (error) {
565
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
566
+ this.logger.debug(`IAM sign-up error for ${input.email}: ${errorMessage}`);
567
+
568
+ if (errorMessage.includes('already exists') || errorMessage.includes('already in use')) {
569
+ throw new BadRequestException('Email address already in use');
570
+ }
571
+ throw error;
572
+ }
573
+ }
335
574
  }