@nauth-toolkit/core 0.1.87 → 0.1.88

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 (163) hide show
  1. package/dist/dto/admin-get-user-auth-history.dto.d.ts +62 -0
  2. package/dist/dto/admin-get-user-auth-history.dto.d.ts.map +1 -0
  3. package/dist/dto/admin-get-user-auth-history.dto.js +87 -0
  4. package/dist/dto/admin-get-user-auth-history.dto.js.map +1 -0
  5. package/dist/dto/admin-logout-all.dto.d.ts +48 -0
  6. package/dist/dto/admin-logout-all.dto.d.ts.map +1 -0
  7. package/dist/dto/{change-password-request.dto.js → admin-logout-all.dto.js} +36 -21
  8. package/dist/dto/admin-logout-all.dto.js.map +1 -0
  9. package/dist/dto/admin-remove-devices.dto.d.ts +25 -0
  10. package/dist/dto/admin-remove-devices.dto.d.ts.map +1 -0
  11. package/dist/dto/admin-remove-devices.dto.js +50 -0
  12. package/dist/dto/admin-remove-devices.dto.js.map +1 -0
  13. package/dist/dto/admin-reset-password.dto.d.ts +15 -19
  14. package/dist/dto/admin-reset-password.dto.d.ts.map +1 -1
  15. package/dist/dto/admin-reset-password.dto.js +21 -41
  16. package/dist/dto/admin-reset-password.dto.js.map +1 -1
  17. package/dist/dto/admin-revoke-session.dto.d.ts +22 -0
  18. package/dist/dto/admin-revoke-session.dto.d.ts.map +1 -0
  19. package/dist/dto/admin-revoke-session.dto.js +48 -0
  20. package/dist/dto/admin-revoke-session.dto.js.map +1 -0
  21. package/dist/dto/admin-set-password.dto.d.ts +8 -10
  22. package/dist/dto/admin-set-password.dto.d.ts.map +1 -1
  23. package/dist/dto/admin-set-password.dto.js +11 -21
  24. package/dist/dto/admin-set-password.dto.js.map +1 -1
  25. package/dist/dto/admin-set-preferred-method.dto.d.ts +25 -0
  26. package/dist/dto/admin-set-preferred-method.dto.d.ts.map +1 -0
  27. package/dist/dto/admin-set-preferred-method.dto.js +50 -0
  28. package/dist/dto/admin-set-preferred-method.dto.js.map +1 -0
  29. package/dist/dto/admin-update-user-attributes.dto.d.ts +41 -0
  30. package/dist/dto/admin-update-user-attributes.dto.d.ts.map +1 -0
  31. package/dist/dto/{update-user-attributes-request.dto.js → admin-update-user-attributes.dto.js} +12 -17
  32. package/dist/dto/admin-update-user-attributes.dto.js.map +1 -0
  33. package/dist/dto/auth-challenge.dto.d.ts +2 -2
  34. package/dist/dto/auth-challenge.dto.d.ts.map +1 -1
  35. package/dist/dto/auth-challenge.dto.js +3 -3
  36. package/dist/dto/auth-challenge.dto.js.map +1 -1
  37. package/dist/dto/auth-response.dto.d.ts +1 -1
  38. package/dist/dto/auth-response.dto.d.ts.map +1 -1
  39. package/dist/dto/auth-response.dto.js +1 -1
  40. package/dist/dto/auth-response.dto.js.map +1 -1
  41. package/dist/dto/get-mfa-status.dto.d.ts +8 -4
  42. package/dist/dto/get-mfa-status.dto.d.ts.map +1 -1
  43. package/dist/dto/get-mfa-status.dto.js +8 -4
  44. package/dist/dto/get-mfa-status.dto.js.map +1 -1
  45. package/dist/dto/get-risk-assessment-history.dto.d.ts +3 -3
  46. package/dist/dto/get-risk-assessment-history.dto.d.ts.map +1 -1
  47. package/dist/dto/get-risk-assessment-history.dto.js +5 -5
  48. package/dist/dto/get-risk-assessment-history.dto.js.map +1 -1
  49. package/dist/dto/get-suspicious-activity.dto.d.ts +3 -3
  50. package/dist/dto/get-suspicious-activity.dto.d.ts.map +1 -1
  51. package/dist/dto/get-suspicious-activity.dto.js +5 -5
  52. package/dist/dto/get-suspicious-activity.dto.js.map +1 -1
  53. package/dist/dto/get-user-auth-history.dto.d.ts +4 -39
  54. package/dist/dto/get-user-auth-history.dto.d.ts.map +1 -1
  55. package/dist/dto/get-user-auth-history.dto.js +53 -51
  56. package/dist/dto/get-user-auth-history.dto.js.map +1 -1
  57. package/dist/dto/get-user-devices.dto.d.ts +5 -18
  58. package/dist/dto/get-user-devices.dto.d.ts.map +1 -1
  59. package/dist/dto/get-user-devices.dto.js +5 -39
  60. package/dist/dto/get-user-devices.dto.js.map +1 -1
  61. package/dist/dto/get-user-sessions-response.dto.d.ts +1 -1
  62. package/dist/dto/get-user-sessions-response.dto.js +1 -1
  63. package/dist/dto/get-user-sessions.dto.d.ts +1 -1
  64. package/dist/dto/get-user-sessions.dto.js +1 -1
  65. package/dist/dto/index.d.ts +8 -2
  66. package/dist/dto/index.d.ts.map +1 -1
  67. package/dist/dto/index.js +8 -2
  68. package/dist/dto/index.js.map +1 -1
  69. package/dist/dto/logout-all-response.dto.d.ts +1 -1
  70. package/dist/dto/logout-all-response.dto.js +1 -1
  71. package/dist/dto/logout-all.dto.d.ts +1 -18
  72. package/dist/dto/logout-all.dto.d.ts.map +1 -1
  73. package/dist/dto/logout-all.dto.js +1 -30
  74. package/dist/dto/logout-all.dto.js.map +1 -1
  75. package/dist/dto/logout-session.dto.d.ts +0 -5
  76. package/dist/dto/logout-session.dto.d.ts.map +1 -1
  77. package/dist/dto/logout-session.dto.js +0 -12
  78. package/dist/dto/logout-session.dto.js.map +1 -1
  79. package/dist/dto/logout.dto.d.ts +1 -18
  80. package/dist/dto/logout.dto.d.ts.map +1 -1
  81. package/dist/dto/logout.dto.js +1 -30
  82. package/dist/dto/logout.dto.js.map +1 -1
  83. package/dist/dto/remove-devices.dto.d.ts +4 -16
  84. package/dist/dto/remove-devices.dto.d.ts.map +1 -1
  85. package/dist/dto/remove-devices.dto.js +4 -26
  86. package/dist/dto/remove-devices.dto.js.map +1 -1
  87. package/dist/dto/set-mfa-exemption.dto.d.ts +8 -9
  88. package/dist/dto/set-mfa-exemption.dto.d.ts.map +1 -1
  89. package/dist/dto/set-mfa-exemption.dto.js +11 -13
  90. package/dist/dto/set-mfa-exemption.dto.js.map +1 -1
  91. package/dist/dto/set-must-change-password.dto.d.ts +3 -3
  92. package/dist/dto/set-must-change-password.dto.d.ts.map +1 -1
  93. package/dist/dto/set-must-change-password.dto.js +5 -5
  94. package/dist/dto/set-must-change-password.dto.js.map +1 -1
  95. package/dist/dto/set-preferred-method.dto.d.ts +4 -16
  96. package/dist/dto/set-preferred-method.dto.d.ts.map +1 -1
  97. package/dist/dto/set-preferred-method.dto.js +4 -26
  98. package/dist/dto/set-preferred-method.dto.js.map +1 -1
  99. package/dist/dto/setup-mfa.dto.d.ts +3 -18
  100. package/dist/dto/setup-mfa.dto.d.ts.map +1 -1
  101. package/dist/dto/setup-mfa.dto.js +3 -30
  102. package/dist/dto/setup-mfa.dto.js.map +1 -1
  103. package/dist/dto/social-auth.dto.d.ts +4 -34
  104. package/dist/dto/social-auth.dto.d.ts.map +1 -1
  105. package/dist/dto/social-auth.dto.js +10 -68
  106. package/dist/dto/social-auth.dto.js.map +1 -1
  107. package/dist/dto/update-user-attributes.dto.d.ts +26 -0
  108. package/dist/dto/update-user-attributes.dto.d.ts.map +1 -0
  109. package/dist/dto/update-user-attributes.dto.js +30 -0
  110. package/dist/dto/update-user-attributes.dto.js.map +1 -0
  111. package/dist/index.d.ts +5 -0
  112. package/dist/index.d.ts.map +1 -1
  113. package/dist/index.js +5 -0
  114. package/dist/index.js.map +1 -1
  115. package/dist/interfaces/hooks.interface.d.ts +2 -1
  116. package/dist/interfaces/hooks.interface.d.ts.map +1 -1
  117. package/dist/interfaces/provider.interface.d.ts +1 -1
  118. package/dist/interfaces/provider.interface.d.ts.map +1 -1
  119. package/dist/services/adaptive-mfa-decision.service.js +2 -2
  120. package/dist/services/adaptive-mfa-decision.service.js.map +1 -1
  121. package/dist/services/admin-auth.service.d.ts +307 -0
  122. package/dist/services/admin-auth.service.d.ts.map +1 -0
  123. package/dist/services/admin-auth.service.js +885 -0
  124. package/dist/services/admin-auth.service.js.map +1 -0
  125. package/dist/services/auth-audit.service.d.ts +16 -16
  126. package/dist/services/auth-audit.service.d.ts.map +1 -1
  127. package/dist/services/auth-audit.service.js +33 -33
  128. package/dist/services/auth-audit.service.js.map +1 -1
  129. package/dist/services/auth-challenge-helper.service.js +3 -3
  130. package/dist/services/auth-challenge-helper.service.js.map +1 -1
  131. package/dist/services/auth-service-internal-helpers.d.ts +2 -2
  132. package/dist/services/auth-service-internal-helpers.d.ts.map +1 -1
  133. package/dist/services/auth-service-internal-helpers.js.map +1 -1
  134. package/dist/services/auth.service.d.ts +122 -438
  135. package/dist/services/auth.service.d.ts.map +1 -1
  136. package/dist/services/auth.service.js +424 -1255
  137. package/dist/services/auth.service.js.map +1 -1
  138. package/dist/services/mfa.service.d.ts +80 -12
  139. package/dist/services/mfa.service.d.ts.map +1 -1
  140. package/dist/services/mfa.service.js +347 -261
  141. package/dist/services/mfa.service.js.map +1 -1
  142. package/dist/services/social-auth.service.d.ts +7 -0
  143. package/dist/services/social-auth.service.d.ts.map +1 -1
  144. package/dist/services/social-auth.service.js +38 -26
  145. package/dist/services/social-auth.service.js.map +1 -1
  146. package/dist/services/user.service.d.ts +3 -3
  147. package/dist/services/user.service.d.ts.map +1 -1
  148. package/dist/services/user.service.js +7 -7
  149. package/dist/services/user.service.js.map +1 -1
  150. package/dist/utils/dto-validator.d.ts.map +1 -1
  151. package/dist/utils/dto-validator.js +50 -4
  152. package/dist/utils/dto-validator.js.map +1 -1
  153. package/dist/utils/setup/init-services.d.ts +2 -1
  154. package/dist/utils/setup/init-services.d.ts.map +1 -1
  155. package/dist/utils/setup/init-services.js +2 -0
  156. package/dist/utils/setup/init-services.js.map +1 -1
  157. package/package.json +1 -1
  158. package/dist/dto/change-password-request.dto.d.ts +0 -43
  159. package/dist/dto/change-password-request.dto.d.ts.map +0 -1
  160. package/dist/dto/change-password-request.dto.js.map +0 -1
  161. package/dist/dto/update-user-attributes-request.dto.d.ts +0 -44
  162. package/dist/dto/update-user-attributes-request.dto.d.ts.map +0 -1
  163. package/dist/dto/update-user-attributes-request.dto.js.map +0 -1
@@ -8,6 +8,7 @@ const auth_challenge_dto_1 = require("../dto/auth-challenge.dto");
8
8
  const auth_audit_event_type_enum_1 = require("../enums/auth-audit-event-type.enum");
9
9
  const dto_validator_1 = require("../utils/dto-validator");
10
10
  const class_validator_1 = require("class-validator");
11
+ const context_storage_1 = require("../utils/context-storage");
11
12
  const dto_1 = require("../dto");
12
13
  /**
13
14
  * MFA Service Registry
@@ -48,6 +49,261 @@ class MFAService {
48
49
  clientInfoService;
49
50
  hookRegistry;
50
51
  providers = new Map();
52
+ // ============================================================================
53
+ // Internal helpers (shared by user + admin APIs)
54
+ // ============================================================================
55
+ /**
56
+ * Fetch active MFA devices for a given internal user ID.
57
+ *
58
+ * @param userId - Internal DB user ID
59
+ * @returns Active MFA devices
60
+ */
61
+ async getActiveDevicesForUserId(userId) {
62
+ const devices = await this.mfaDeviceRepository.find({
63
+ where: { userId, isActive: true },
64
+ order: { createdAt: 'DESC' },
65
+ });
66
+ return devices;
67
+ }
68
+ /**
69
+ * Resolve a target user by `sub` (admin-style targeting).
70
+ *
71
+ * @param sub - Target user sub (UUID v4)
72
+ * @returns User entity
73
+ * @throws {NAuthException} NOT_FOUND when user is not found
74
+ */
75
+ async getUserBySubOrThrow(sub) {
76
+ const user = (await this.userRepository.findOne({
77
+ where: { sub },
78
+ }));
79
+ if (!user) {
80
+ throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found');
81
+ }
82
+ return user;
83
+ }
84
+ /**
85
+ * Shared implementation for removing MFA devices.
86
+ *
87
+ * @param targetUser - Target user (self-service or admin target)
88
+ * @param methodType - MFA method to remove (normalized)
89
+ * @param removedBy - Actor performing the removal
90
+ */
91
+ async removeDevicesInternal(targetUser, methodType, removedBy) {
92
+ const userId = targetUser.id;
93
+ const preferredMethod = targetUser.preferredMfaMethod;
94
+ const isPreferredMethod = preferredMethod === methodType;
95
+ // Get all active devices for this user
96
+ const activeDevices = await this.getActiveDevicesForUserId(userId);
97
+ // Get devices of the method type to remove
98
+ const devicesToRemove = activeDevices.filter((d) => d.type.toLowerCase() === methodType);
99
+ if (devicesToRemove.length === 0) {
100
+ throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.VALIDATION_FAILED, `No active ${methodType} MFA devices found for this user`);
101
+ }
102
+ // Delete all devices of this method type
103
+ let deletedCount = 0;
104
+ for (const device of devicesToRemove) {
105
+ const result = await this.mfaDeviceRepository.delete(device.id);
106
+ deletedCount += result.affected || 0;
107
+ }
108
+ // Check if any devices remain after removal
109
+ const remainingActiveDevices = await this.getActiveDevicesForUserId(userId);
110
+ let mfaDisabled = false;
111
+ // If no active devices remain, disable MFA for user
112
+ if (remainingActiveDevices.length === 0) {
113
+ await this.userRepository.update({ id: userId }, {
114
+ mfaEnabled: false,
115
+ mfaMethods: [],
116
+ preferredMfaMethod: null,
117
+ });
118
+ mfaDisabled = true;
119
+ // ============================================================================
120
+ // Audit: Record MFA disabled (all devices removed)
121
+ // ============================================================================
122
+ if (this.auditService && this.clientInfoService) {
123
+ try {
124
+ await this.auditService?.recordEvent({
125
+ userId,
126
+ eventType: auth_audit_event_type_enum_1.AuthAuditEventType.MFA_DISABLED,
127
+ eventStatus: 'INFO',
128
+ reason: removedBy === 'admin' ? 'admin_action' : 'all_devices_removed',
129
+ description: removedBy === 'admin'
130
+ ? 'MFA disabled by admin - all devices removed'
131
+ : 'MFA disabled - all devices removed',
132
+ // Client info automatically included from context
133
+ metadata: {
134
+ removedMethod: methodType,
135
+ deletedCount,
136
+ removedBy,
137
+ },
138
+ });
139
+ }
140
+ catch (auditError) {
141
+ const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
142
+ this.logger?.error?.(`Failed to record MFA_DISABLED audit event: ${errorMessage}`, {
143
+ error: auditError,
144
+ userId,
145
+ });
146
+ }
147
+ }
148
+ // Automatically create MFA_SETUP_REQUIRED challenge if MFA enforcement requires it
149
+ if (this.challengeService && this.config?.mfa?.enabled) {
150
+ const enforcement = this.config.mfa.enforcement || 'OPTIONAL';
151
+ if (enforcement === 'REQUIRED' || enforcement === 'ADAPTIVE') {
152
+ try {
153
+ await this.challengeService.createChallengeSession(targetUser, auth_challenge_dto_1.AuthChallenge.MFA_SETUP_REQUIRED, {
154
+ allowedMethods: this.config.mfa.allowedMethods || [],
155
+ requiresSetup: true,
156
+ });
157
+ this.logger?.log?.(`Created MFA_SETUP_REQUIRED challenge for user ${targetUser.sub} after MFA removal`);
158
+ }
159
+ catch (error) {
160
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
161
+ this.logger?.warn?.(`Failed to create MFA_SETUP_REQUIRED challenge after MFA removal: ${errorMessage}`);
162
+ }
163
+ }
164
+ }
165
+ }
166
+ else {
167
+ // Update mfaMethods array with remaining methods
168
+ const remainingMethods = [...new Set(remainingActiveDevices.map((d) => d.type))];
169
+ // If the removed method was preferred, update preferred method and device primary flags
170
+ if (isPreferredMethod) {
171
+ const newPreferredMethod = remainingActiveDevices[0].type;
172
+ await this.userRepository.update({ id: userId }, {
173
+ mfaMethods: remainingMethods,
174
+ preferredMfaMethod: newPreferredMethod,
175
+ });
176
+ // Update device primary flags - set first remaining device as primary
177
+ if (remainingActiveDevices[0].id) {
178
+ await this.mfaDeviceRepository.update({ id: remainingActiveDevices[0].id }, { isPrimary: true });
179
+ }
180
+ // Unset primary flag on other devices
181
+ for (let i = 1; i < remainingActiveDevices.length; i++) {
182
+ if (remainingActiveDevices[i].id) {
183
+ await this.mfaDeviceRepository.update({ id: remainingActiveDevices[i].id }, { isPrimary: false });
184
+ }
185
+ }
186
+ this.logger?.log?.(`Updated preferred MFA method to ${newPreferredMethod} after removing ${methodType}`);
187
+ }
188
+ else {
189
+ // No preferred method change needed, just update mfaMethods
190
+ await this.userRepository.update({ id: userId }, {
191
+ mfaMethods: remainingMethods,
192
+ });
193
+ }
194
+ }
195
+ // ============================================================================
196
+ // Audit: Record MFA device removal
197
+ // ============================================================================
198
+ if (deletedCount > 0 && this.auditService && this.clientInfoService) {
199
+ try {
200
+ await this.auditService?.recordEvent({
201
+ userId,
202
+ eventType: auth_audit_event_type_enum_1.AuthAuditEventType.MFA_DEVICE_REMOVED,
203
+ eventStatus: 'INFO',
204
+ metadata: {
205
+ method: methodType,
206
+ deletedCount,
207
+ remainingDevices: remainingActiveDevices.length,
208
+ mfaDisabled,
209
+ removedBy,
210
+ },
211
+ // Client info automatically included from context
212
+ });
213
+ }
214
+ catch (auditError) {
215
+ const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
216
+ this.logger?.error?.(`Failed to record MFA_DEVICE_REMOVED audit event: ${errorMessage}`, {
217
+ error: auditError,
218
+ userId,
219
+ method: methodType,
220
+ });
221
+ }
222
+ }
223
+ // ============================================================================
224
+ // Lifecycle Hook: MFA Device Removed
225
+ // ============================================================================
226
+ if (deletedCount > 0 && this.hookRegistry && this.clientInfoService) {
227
+ try {
228
+ const clientInfo = this.clientInfoService.get();
229
+ await this.hookRegistry.executeMFADeviceRemoved({
230
+ user: targetUser,
231
+ deviceType: methodType,
232
+ removedBy,
233
+ remainingDeviceCount: remainingActiveDevices.length,
234
+ clientInfo: {
235
+ ipAddress: clientInfo.ipAddress,
236
+ userAgent: clientInfo.userAgent,
237
+ ipCountry: clientInfo.ipCountry,
238
+ ipCity: clientInfo.ipCity,
239
+ },
240
+ });
241
+ }
242
+ catch (hookError) {
243
+ const errorMessage = hookError instanceof Error ? hookError.message : 'Unknown error';
244
+ this.logger?.error?.(`Failed to execute mfaDeviceRemoved hooks: ${errorMessage}`, {
245
+ error: hookError,
246
+ userId,
247
+ method: methodType,
248
+ });
249
+ }
250
+ }
251
+ return { deletedCount, mfaDisabled };
252
+ }
253
+ /**
254
+ * Shared implementation for setting preferred MFA method.
255
+ *
256
+ * @param targetUser - Target user (self-service or admin target)
257
+ * @param methodType - Preferred method (normalized)
258
+ * @param updatedBy - Actor performing the update
259
+ */
260
+ async setPreferredMethodInternal(targetUser, methodType, updatedBy) {
261
+ // Verify user has this method configured
262
+ const activeDevices = await this.getActiveDevicesForUserId(targetUser.id);
263
+ const preferredDevice = activeDevices.find((d) => d.type.toLowerCase() === methodType && d.isActive);
264
+ if (!preferredDevice) {
265
+ throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.VALIDATION_FAILED, `MFA method '${methodType}' is not configured for this user`);
266
+ }
267
+ // Update user's preferred method
268
+ await this.userRepository.update({ id: targetUser.id }, {
269
+ preferredMfaMethod: methodType,
270
+ });
271
+ // Update device isPrimary flags: set preferred device as primary, unset others
272
+ for (const device of activeDevices) {
273
+ await this.mfaDeviceRepository.update({ id: device.id }, { isPrimary: device.id === preferredDevice.id });
274
+ }
275
+ this.logger?.log?.(`Device ${preferredDevice.id} set as primary for user ${targetUser.sub} (by ${updatedBy})`);
276
+ // ============================================================================
277
+ // Audit: Record preferred MFA method update
278
+ // ============================================================================
279
+ if (this.auditService && this.clientInfoService) {
280
+ try {
281
+ const previousMethod = targetUser.preferredMfaMethod;
282
+ await this.auditService?.recordEvent({
283
+ userId: targetUser.id,
284
+ eventType: auth_audit_event_type_enum_1.AuthAuditEventType.MFA_PREFERRED_METHOD_UPDATED,
285
+ eventStatus: 'INFO',
286
+ metadata: {
287
+ previousMethod: previousMethod || null,
288
+ newMethod: methodType,
289
+ deviceId: preferredDevice.id,
290
+ updatedBy,
291
+ },
292
+ });
293
+ }
294
+ catch (auditError) {
295
+ const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
296
+ this.logger?.error?.(`Failed to record MFA_PREFERRED_METHOD_UPDATED audit event: ${errorMessage}`, {
297
+ error: auditError,
298
+ userId: targetUser.id,
299
+ method: methodType,
300
+ });
301
+ }
302
+ }
303
+ return {
304
+ message: 'Preferred method updated',
305
+ };
306
+ }
51
307
  /**
52
308
  * Resolve a user entity by flexible identifier.
53
309
  *
@@ -98,6 +354,19 @@ class MFAService {
98
354
  this.clientInfoService = clientInfoService;
99
355
  this.hookRegistry = hookRegistry;
100
356
  }
357
+ /**
358
+ * Get current user from authenticated context
359
+ *
360
+ * @returns Current authenticated user
361
+ * @throws {NAuthException} If user not found in context
362
+ */
363
+ getCurrentUserOrThrow() {
364
+ const currentUser = context_storage_1.ContextStorage.get('CURRENT_USER');
365
+ if (!currentUser) {
366
+ throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.FORBIDDEN, 'Authentication required');
367
+ }
368
+ return currentUser;
369
+ }
101
370
  /**
102
371
  * Register an MFA provider
103
372
  *
@@ -282,12 +551,8 @@ class MFAService {
282
551
  */
283
552
  async setup(dto) {
284
553
  dto = await (0, dto_validator_1.ensureValidatedDto)(dto_1.SetupMFADTO, dto);
285
- // Look up user by sub
286
- const userEntity = await this.userRepository.findOne({ where: { sub: dto.sub } });
287
- if (!userEntity) {
288
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found');
289
- }
290
- const user = userEntity;
554
+ // Get user from authenticated context (already has all fields)
555
+ const user = this.getCurrentUserOrThrow();
291
556
  const provider = this.getProvider(dto.methodName);
292
557
  const setupData = await provider.setup(user, dto.setupData);
293
558
  return {
@@ -297,29 +562,23 @@ class MFAService {
297
562
  /**
298
563
  * Get user's MFA devices
299
564
  *
300
- * @param dto - Request DTO with user sub
565
+ * User self-service method: current user is derived from authenticated context.
566
+ *
567
+ * @param _dto - Optional (empty) DTO for validation consistency
301
568
  * @returns Response DTO with array of MFA devices
302
569
  *
303
570
  * @example
304
571
  * ```typescript
305
- * const result = await this.mfaService.getUserDevices({ sub: user.sub });
572
+ * const result = await this.mfaService.getUserDevices();
306
573
  * // Returns: { devices: [...] }
307
574
  * ```
308
575
  */
309
- async getUserDevices(dto) {
310
- dto = await (0, dto_validator_1.ensureValidatedDto)(dto_1.GetUserDevicesDTO, dto);
311
- // Look up user by sub to get internal ID
312
- const userEntity = await this.userRepository.findOne({ where: { sub: dto.sub } });
313
- if (!userEntity) {
314
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found');
315
- }
316
- // Only fetch active devices (inactive devices are soft-deleted)
317
- const devices = await this.mfaDeviceRepository.find({
318
- where: { userId: userEntity.id, isActive: true },
319
- order: { createdAt: 'DESC' },
320
- });
576
+ async getUserDevices(_dto = {}) {
577
+ await (0, dto_validator_1.ensureValidatedDto)(dto_1.GetUserDevicesDTO, _dto);
578
+ // Get user from authenticated context (already has id and sub)
579
+ const currentUser = this.getCurrentUserOrThrow();
321
580
  return {
322
- devices: devices,
581
+ devices: await this.getActiveDevicesForUserId(currentUser.id),
323
582
  };
324
583
  }
325
584
  /**
@@ -376,9 +635,16 @@ class MFAService {
376
635
  }
377
636
  }
378
637
  // Get user's configured devices
379
- const devicesResult = await this.getUserDevices({ sub: dto.sub });
638
+ // NOTE:
639
+ // `getUserDevices()` is a self-service method that uses CURRENT_USER from context.
640
+ // Here we must load devices for the target user identified by `dto.sub` (admin/user lookup),
641
+ // so we query by the resolved internal user ID.
642
+ const devicesResult = await this.mfaDeviceRepository.find({
643
+ where: { userId: userEntity.id, isActive: true },
644
+ order: { createdAt: 'DESC' },
645
+ });
380
646
  const configuredMethods = [
381
- ...new Set(devicesResult.devices.filter((d) => d.isActive).map((d) => d.type)),
647
+ ...new Set(devicesResult.filter((d) => d.isActive).map((d) => d.type)),
382
648
  ];
383
649
  // Determine if MFA is required based on config and user state
384
650
  const required = enabled && configuredMethods.length > 0;
@@ -400,7 +666,7 @@ class MFAService {
400
666
  * Remove MFA devices by method type
401
667
  *
402
668
  * Comprehensive method that handles all aspects of MFA device removal:
403
- * - Looks up user by sub (consumer apps should pass user.sub from @CurrentUser())
669
+ * - Uses the authenticated user context (self-service)
404
670
  * - Validates method type
405
671
  * - Removes all active devices of the specified method type
406
672
  * - Updates user's preferred method if the removed method was preferred
@@ -411,7 +677,7 @@ class MFAService {
411
677
  * This method encapsulates all database operations related to MFA device removal,
412
678
  * ensuring the consumer app doesn't need to directly manipulate nauth_* tables.
413
679
  *
414
- * @param dto - Request DTO with user sub and method type
680
+ * @param dto - Request DTO with method type
415
681
  * @returns Response DTO with deletedCount and whether MFA was disabled
416
682
  * @throws {NAuthException} If user not found, invalid method type, or no devices found
417
683
  *
@@ -420,189 +686,43 @@ class MFAService {
420
686
  * // Consumer app controller
421
687
  * @Delete('mfa/devices/:method')
422
688
  * async removeMFAMethod(@CurrentUser() user: IUser, @Param('method') method: string) {
423
- * const result = await this.mfaService.removeDevices({ userSub: user.sub, methodType: method });
689
+ * const result = await this.mfaService.removeDevices({ methodType: method });
424
690
  * return { message: 'MFA method removed successfully', ...result };
425
691
  * }
426
692
  * ```
427
693
  */
428
694
  async removeDevices(dto) {
429
695
  dto = await (0, dto_validator_1.ensureValidatedDto)(dto_1.RemoveDevicesDTO, dto);
430
- // Validate method type
431
696
  const validMethods = [mfa_method_enum_1.MFAMethod.TOTP, mfa_method_enum_1.MFAMethod.SMS, mfa_method_enum_1.MFAMethod.EMAIL, mfa_method_enum_1.MFAMethod.PASSKEY];
432
697
  const normalizedMethod = dto.methodType.toLowerCase();
433
698
  if (!validMethods.includes(normalizedMethod)) {
434
699
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.VALIDATION_FAILED, `Invalid MFA method: ${dto.methodType}. Valid methods are: ${validMethods.join(', ')}`);
435
700
  }
436
- // Look up user by sub using repository directly (no AuthService dependency needed)
437
- const userEntity = await this.userRepository.findOne({ where: { sub: dto.userSub } });
438
- if (!userEntity) {
439
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User entity not found');
440
- }
441
- const userId = userEntity.id;
442
- if (!userId) {
443
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.INTERNAL_ERROR, 'User entity missing internal ID');
444
- }
445
- // Cast to IUser for type safety
446
- const user = userEntity;
447
- const preferredMethod = userEntity.preferredMfaMethod;
448
- const isPreferredMethod = preferredMethod === normalizedMethod;
449
- // Get all active devices for this user
450
- const devicesResult = await this.getUserDevices({ sub: dto.userSub });
451
- const activeDevices = devicesResult.devices.filter((d) => d.isActive);
452
- // Get devices of the method type to remove
453
- const devicesToRemove = activeDevices.filter((d) => d.type.toLowerCase() === normalizedMethod);
454
- if (devicesToRemove.length === 0) {
455
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.VALIDATION_FAILED, `No active ${normalizedMethod} MFA devices found for this user`);
456
- }
457
- // Delete all devices of this method type
458
- let deletedCount = 0;
459
- for (const device of devicesToRemove) {
460
- const result = await this.mfaDeviceRepository.delete(device.id);
461
- deletedCount += result.affected || 0;
462
- }
463
- // Check if any devices remain after removal
464
- const remainingDevicesResult = await this.getUserDevices({ sub: dto.userSub });
465
- const remainingActiveDevices = remainingDevicesResult.devices.filter((d) => d.isActive);
466
- let mfaDisabled = false;
467
- // If no active devices remain, disable MFA for user
468
- if (remainingActiveDevices.length === 0) {
469
- userEntity.mfaEnabled = false;
470
- userEntity.mfaMethods = [];
471
- userEntity.preferredMfaMethod = null;
472
- await this.userRepository.save(userEntity);
473
- mfaDisabled = true;
474
- // ============================================================================
475
- // Audit: Record MFA disabled (all devices removed)
476
- // ============================================================================
477
- if (this.auditService && this.clientInfoService) {
478
- try {
479
- await this.auditService?.recordEvent({
480
- userId: user.id,
481
- eventType: auth_audit_event_type_enum_1.AuthAuditEventType.MFA_DISABLED,
482
- eventStatus: 'INFO',
483
- reason: 'all_devices_removed',
484
- description: 'MFA disabled - all devices removed',
485
- // Client info automatically included from context
486
- metadata: {
487
- removedMethod: normalizedMethod,
488
- deletedCount,
489
- },
490
- });
491
- }
492
- catch (auditError) {
493
- // Non-blocking: Log but continue
494
- const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
495
- this.logger?.error?.(`Failed to record MFA_DISABLED audit event: ${errorMessage}`, {
496
- error: auditError,
497
- userId: user.id,
498
- });
499
- }
500
- }
501
- // Automatically create MFA_SETUP_REQUIRED challenge if MFA enforcement requires it
502
- if (this.challengeService && this.config?.mfa?.enabled) {
503
- const enforcement = this.config.mfa.enforcement || 'OPTIONAL';
504
- if (enforcement === 'REQUIRED' || enforcement === 'ADAPTIVE') {
505
- const user = userEntity;
506
- try {
507
- // Client info (ipAddress, userAgent) automatically extracted from ClientInfoService
508
- await this.challengeService.createChallengeSession(user, auth_challenge_dto_1.AuthChallenge.MFA_SETUP_REQUIRED, {
509
- allowedMethods: this.config.mfa.allowedMethods || [],
510
- requiresSetup: true,
511
- });
512
- this.logger?.log?.(`Created MFA_SETUP_REQUIRED challenge for user ${user.sub} after MFA removal`);
513
- }
514
- catch (error) {
515
- // Log but don't fail the removal if challenge creation fails
516
- this.logger?.warn?.(`Failed to create MFA_SETUP_REQUIRED challenge after MFA removal: ${error}`);
517
- }
518
- }
519
- }
520
- }
521
- else {
522
- // Update mfaMethods array with remaining methods
523
- const remainingMethods = [...new Set(remainingActiveDevices.map((d) => d.type))];
524
- userEntity.mfaMethods = remainingMethods;
525
- // If the removed method was preferred, update preferred method and device primary flags
526
- if (isPreferredMethod) {
527
- const newPreferredMethod = remainingActiveDevices[0].type;
528
- userEntity.preferredMfaMethod = newPreferredMethod;
529
- await this.userRepository.save(userEntity);
530
- // Update device primary flags - set first remaining device as primary
531
- if (remainingActiveDevices[0].id) {
532
- await this.mfaDeviceRepository.update({ id: remainingActiveDevices[0].id }, { isPrimary: true });
533
- }
534
- // Unset primary flag on other devices
535
- for (let i = 1; i < remainingActiveDevices.length; i++) {
536
- if (remainingActiveDevices[i].id) {
537
- await this.mfaDeviceRepository.update({ id: remainingActiveDevices[i].id }, { isPrimary: false });
538
- }
539
- }
540
- this.logger?.log?.(`Updated preferred MFA method to ${newPreferredMethod} after removing ${normalizedMethod}`);
541
- }
542
- else {
543
- // No preferred method change needed, just update mfaMethods
544
- await this.userRepository.save(userEntity);
545
- }
546
- }
547
- // ============================================================================
548
- // Audit: Record MFA device removal
549
- // ============================================================================
550
- if (deletedCount > 0 && this.auditService && this.clientInfoService) {
551
- try {
552
- const user = userEntity;
553
- await this.auditService?.recordEvent({
554
- userId: user.id,
555
- eventType: auth_audit_event_type_enum_1.AuthAuditEventType.MFA_DEVICE_REMOVED,
556
- eventStatus: 'INFO',
557
- metadata: {
558
- method: normalizedMethod,
559
- deletedCount,
560
- remainingDevices: remainingActiveDevices.length,
561
- mfaDisabled,
562
- },
563
- // Client info automatically included from context
564
- });
565
- }
566
- catch (auditError) {
567
- // Non-blocking: Log but continue
568
- const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
569
- this.logger?.error?.(`Failed to record MFA_DEVICE_REMOVED audit event: ${errorMessage}`, {
570
- error: auditError,
571
- userId: user.id,
572
- method: normalizedMethod,
573
- });
574
- }
575
- }
576
- // ============================================================================
577
- // Lifecycle Hook: MFA Device Removed
578
- // ============================================================================
579
- if (deletedCount > 0 && this.hookRegistry && this.clientInfoService) {
580
- try {
581
- const clientInfo = this.clientInfoService.get();
582
- await this.hookRegistry.executeMFADeviceRemoved({
583
- user,
584
- deviceType: normalizedMethod,
585
- removedBy: 'user',
586
- remainingDeviceCount: remainingActiveDevices.length,
587
- clientInfo: {
588
- ipAddress: clientInfo.ipAddress,
589
- userAgent: clientInfo.userAgent,
590
- ipCountry: clientInfo.ipCountry,
591
- ipCity: clientInfo.ipCity,
592
- },
593
- });
594
- }
595
- catch (hookError) {
596
- // Non-blocking: Log but continue
597
- const errorMessage = hookError instanceof Error ? hookError.message : 'Unknown error';
598
- this.logger?.error?.(`Failed to execute mfaDeviceRemoved hooks: ${errorMessage}`, {
599
- error: hookError,
600
- userId: user.id,
601
- method: normalizedMethod,
602
- });
603
- }
701
+ const currentUser = this.getCurrentUserOrThrow();
702
+ return await this.removeDevicesInternal(currentUser, normalizedMethod, 'user');
703
+ }
704
+ /**
705
+ * Admin: Remove MFA devices for a specific user by `sub`.
706
+ *
707
+ * @param dto - Admin DTO containing target `sub` and method type
708
+ * @returns Removal result
709
+ * @throws {NAuthException} NOT_FOUND when user is not found
710
+ * @throws {NAuthException} VALIDATION_FAILED on invalid method type
711
+ *
712
+ * @example
713
+ * ```typescript
714
+ * await mfaService.adminRemoveDevices({ sub: 'user-uuid', methodType: 'totp' });
715
+ * ```
716
+ */
717
+ async adminRemoveDevices(dto) {
718
+ dto = await (0, dto_validator_1.ensureValidatedDto)(dto_1.AdminRemoveDevicesDTO, dto);
719
+ const validMethods = [mfa_method_enum_1.MFAMethod.TOTP, mfa_method_enum_1.MFAMethod.SMS, mfa_method_enum_1.MFAMethod.EMAIL, mfa_method_enum_1.MFAMethod.PASSKEY];
720
+ const normalizedMethod = dto.methodType.toLowerCase();
721
+ if (!validMethods.includes(normalizedMethod)) {
722
+ throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.VALIDATION_FAILED, `Invalid MFA method: ${dto.methodType}. Valid methods are: ${validMethods.join(', ')}`);
604
723
  }
605
- return { deletedCount, mfaDisabled };
724
+ const targetUser = await this.getUserBySubOrThrow(dto.sub);
725
+ return await this.removeDevicesInternal(targetUser, normalizedMethod, 'admin');
606
726
  }
607
727
  /**
608
728
  * Set preferred MFA method for a user
@@ -613,7 +733,7 @@ class MFAService {
613
733
  * This method encapsulates all database operations related to preferred method updates,
614
734
  * ensuring the consumer app doesn't need to directly manipulate nauth_* tables.
615
735
  *
616
- * @param dto - Request DTO with user sub and method type
736
+ * @param dto - Request DTO with method type
617
737
  * @returns Response DTO with success message
618
738
  * @throws {NAuthException} If user not found, invalid method type, or method not configured
619
739
  *
@@ -622,7 +742,7 @@ class MFAService {
622
742
  * // Consumer app controller
623
743
  * @Put('mfa/preferred')
624
744
  * async setPreferredMFAMethod(@CurrentUser() user: IUser, @Body() body: { method: string }) {
625
- * return await this.mfaService.setPreferredMethod({ userSub: user.sub, methodType: body.method });
745
+ * return await this.mfaService.setPreferredMethod({ methodType: body.method });
626
746
  * }
627
747
  * ```
628
748
  */
@@ -634,65 +754,31 @@ class MFAService {
634
754
  if (!validMethods.includes(normalizedMethod)) {
635
755
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.VALIDATION_FAILED, `Invalid MFA method: ${dto.methodType}. Valid methods are: ${validMethods.join(', ')}`);
636
756
  }
637
- // Look up user by sub using repository directly (no AuthService dependency needed)
638
- const userEntity = await this.userRepository.findOne({ where: { sub: dto.userSub } });
639
- if (!userEntity) {
640
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found');
641
- }
642
- const userId = userEntity.id;
643
- if (!userId) {
644
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.INTERNAL_ERROR, 'User entity missing internal ID');
645
- }
646
- // Cast to IUser for type safety
647
- const user = userEntity;
648
- // Verify user has this method configured
649
- const devicesResult = await this.getUserDevices({ sub: dto.userSub });
650
- // Normalize device types for comparison (database might store in different case)
651
- const preferredDevice = devicesResult.devices.find((d) => d.type.toLowerCase() === normalizedMethod && d.isActive);
652
- if (!preferredDevice) {
653
- throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.VALIDATION_FAILED, `MFA method '${normalizedMethod}' is not configured for this user`);
654
- }
655
- // Update user's preferred method directly via repository
656
- await this.userRepository.update({ id: userId }, {
657
- preferredMfaMethod: normalizedMethod,
658
- });
659
- // Update device isPrimary flags: set preferred device as primary, unset others
660
- const activeDevices = devicesResult.devices.filter((d) => d.isActive);
661
- for (const device of activeDevices) {
662
- await this.mfaDeviceRepository.update({ id: device.id }, { isPrimary: device.id === preferredDevice.id });
663
- }
664
- this.logger?.log?.(`Device ${preferredDevice.id} set as primary for user ${dto.userSub}`);
665
- // ============================================================================
666
- // Audit: Record preferred MFA method update
667
- // ============================================================================
668
- if (this.auditService && this.clientInfoService) {
669
- try {
670
- const previousMethod = userEntity.preferredMfaMethod;
671
- await this.auditService?.recordEvent({
672
- userId: user.id,
673
- eventType: auth_audit_event_type_enum_1.AuthAuditEventType.MFA_PREFERRED_METHOD_UPDATED,
674
- eventStatus: 'INFO',
675
- metadata: {
676
- // Client info automatically included from context
677
- previousMethod: previousMethod || null,
678
- newMethod: normalizedMethod,
679
- deviceId: preferredDevice.id,
680
- },
681
- });
682
- }
683
- catch (auditError) {
684
- // Non-blocking: Log but continue
685
- const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
686
- this.logger?.error?.(`Failed to record MFA_PREFERRED_METHOD_UPDATED audit event: ${errorMessage}`, {
687
- error: auditError,
688
- userId: user.id,
689
- method: normalizedMethod,
690
- });
691
- }
757
+ const currentUser = this.getCurrentUserOrThrow();
758
+ return await this.setPreferredMethodInternal(currentUser, normalizedMethod, 'user');
759
+ }
760
+ /**
761
+ * Admin: Set preferred MFA method for a specific user by `sub`.
762
+ *
763
+ * @param dto - Admin DTO containing target `sub` and method type
764
+ * @returns Success response
765
+ * @throws {NAuthException} NOT_FOUND when user is not found
766
+ * @throws {NAuthException} VALIDATION_FAILED when method is invalid or not configured
767
+ *
768
+ * @example
769
+ * ```typescript
770
+ * await mfaService.adminSetPreferredMethod({ sub: 'user-uuid', methodType: 'sms' });
771
+ * ```
772
+ */
773
+ async adminSetPreferredMethod(dto) {
774
+ dto = await (0, dto_validator_1.ensureValidatedDto)(dto_1.AdminSetPreferredMethodDTO, dto);
775
+ const validMethods = [mfa_method_enum_1.MFAMethod.TOTP, mfa_method_enum_1.MFAMethod.SMS, mfa_method_enum_1.MFAMethod.EMAIL, mfa_method_enum_1.MFAMethod.PASSKEY];
776
+ const normalizedMethod = dto.methodType.toLowerCase();
777
+ if (!validMethods.includes(normalizedMethod)) {
778
+ throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.VALIDATION_FAILED, `Invalid MFA method: ${dto.methodType}. Valid methods are: ${validMethods.join(', ')}`);
692
779
  }
693
- return {
694
- message: 'Preferred method updated',
695
- };
780
+ const targetUser = await this.getUserBySubOrThrow(dto.sub);
781
+ return await this.setPreferredMethodInternal(targetUser, normalizedMethod, 'admin');
696
782
  }
697
783
  /**
698
784
  * Grant or revoke a user's exemption from multi-factor authentication (MFA) requirements.
@@ -700,7 +786,7 @@ class MFAService {
700
786
  * SECURITY: This admin-only operation updates the user's MFA exemption status, logs the action,
701
787
  * and records an audit event. MFA exemption bypasses MFA at login, but all other security controls remain enforced.
702
788
  *
703
- * @param dto - Request DTO with identifier, exempt flag, reason, and grantedBy
789
+ * @param dto - Request DTO with sub, exempt flag, reason, and grantedBy
704
790
  * @returns Response DTO with updated exemption fields
705
791
  * @throws {NAuthException} If the user is not found
706
792
  *
@@ -708,7 +794,7 @@ class MFAService {
708
794
  * ```typescript
709
795
  * // Grant MFA exemption
710
796
  * await mfaService.setMFAExemption({
711
- * identifier: 'user@example.com',
797
+ * sub: 'a21b654c-2746-4168-acee-c175083a65cd',
712
798
  * exempt: true,
713
799
  * reason: 'Business partner requires MFA bypass',
714
800
  * grantedBy: 'admin@example.com'
@@ -716,7 +802,7 @@ class MFAService {
716
802
  *
717
803
  * // Revoke MFA exemption
718
804
  * await mfaService.setMFAExemption({
719
- * identifier: 'user@example.com',
805
+ * sub: 'a21b654c-2746-4168-acee-c175083a65cd',
720
806
  * exempt: false,
721
807
  * reason: 'MFA now mandatory for this user',
722
808
  * grantedBy: 'admin@example.com'
@@ -728,8 +814,8 @@ class MFAService {
728
814
  // ============================================================================
729
815
  // SECURITY: Resolve the TARGET user from the DTO (admin-only API)
730
816
  // ============================================================================
731
- // Use `identifier` (email/username/phone/sub).
732
- const userEntity = await this.findUserByIdentifier(dto.identifier);
817
+ // Use `sub` (UUID v4) per ADMIN_USER_API_SEPARATION_PLAN.md
818
+ const userEntity = await this.userRepository.findOne({ where: { sub: dto.sub } });
733
819
  if (!userEntity) {
734
820
  throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found');
735
821
  }