@nauth-toolkit/core 0.1.108 → 0.1.111
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/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js.map +1 -1
- package/dist/dto/admin-remove-device.dto.d.ts +22 -0
- package/dist/dto/admin-remove-device.dto.d.ts.map +1 -0
- package/dist/dto/{admin-remove-devices.dto.js → admin-remove-device.dto.js} +21 -21
- package/dist/dto/admin-remove-device.dto.js.map +1 -0
- package/dist/dto/admin-set-preferred-device.dto.d.ts +32 -0
- package/dist/dto/admin-set-preferred-device.dto.d.ts.map +1 -0
- package/dist/dto/admin-set-preferred-device.dto.js +64 -0
- package/dist/dto/admin-set-preferred-device.dto.js.map +1 -0
- package/dist/dto/admin-signup-social.dto.d.ts +2 -2
- package/dist/dto/admin-signup.dto.d.ts +2 -2
- package/dist/dto/challenge-response.dto.d.ts +4 -0
- package/dist/dto/challenge-response.dto.d.ts.map +1 -1
- package/dist/dto/disable-user.dto.d.ts +2 -2
- package/dist/dto/enable-user.dto.d.ts +2 -2
- package/dist/dto/get-user-devices.dto.d.ts +5 -3
- package/dist/dto/get-user-devices.dto.d.ts.map +1 -1
- package/dist/dto/get-user-devices.dto.js +3 -1
- package/dist/dto/get-user-devices.dto.js.map +1 -1
- package/dist/dto/get-user-response.dto.d.ts +1 -1
- package/dist/dto/get-user-response.dto.js +1 -1
- package/dist/dto/get-users.dto.d.ts +2 -2
- package/dist/dto/index.d.ts +6 -4
- package/dist/dto/index.d.ts.map +1 -1
- package/dist/dto/index.js +6 -4
- package/dist/dto/index.js.map +1 -1
- package/dist/dto/mfa-device-response.dto.d.ts +68 -0
- package/dist/dto/mfa-device-response.dto.d.ts.map +1 -0
- package/dist/dto/mfa-device-response.dto.js +81 -0
- package/dist/dto/mfa-device-response.dto.js.map +1 -0
- package/dist/dto/remove-device.dto.d.ts +49 -0
- package/dist/dto/remove-device.dto.d.ts.map +1 -0
- package/dist/dto/remove-device.dto.js +76 -0
- package/dist/dto/remove-device.dto.js.map +1 -0
- package/dist/dto/respond-challenge.dto.d.ts +8 -0
- package/dist/dto/respond-challenge.dto.d.ts.map +1 -1
- package/dist/dto/respond-challenge.dto.js +14 -0
- package/dist/dto/respond-challenge.dto.js.map +1 -1
- package/dist/dto/set-preferred-device.dto.d.ts +46 -0
- package/dist/dto/set-preferred-device.dto.d.ts.map +1 -0
- package/dist/dto/set-preferred-device.dto.js +74 -0
- package/dist/dto/set-preferred-device.dto.js.map +1 -0
- package/dist/dto/setup-mfa.dto.d.ts.map +1 -1
- package/dist/dto/setup-mfa.dto.js.map +1 -1
- package/dist/dto/user-response.dto.d.ts +3 -3
- package/dist/dto/user-response.dto.js +5 -5
- package/dist/dto/verify-mfa-setup-response.dto.d.ts +18 -0
- package/dist/dto/verify-mfa-setup-response.dto.d.ts.map +1 -0
- package/dist/dto/verify-mfa-setup-response.dto.js +22 -0
- package/dist/dto/verify-mfa-setup-response.dto.js.map +1 -0
- package/dist/handlers/client-info.handler.d.ts.map +1 -1
- package/dist/handlers/client-info.handler.js.map +1 -1
- package/dist/interfaces/provider.interface.d.ts.map +1 -1
- package/dist/openapi/components.schemas.json +112 -140
- package/dist/services/admin-auth.service.d.ts +12 -5
- package/dist/services/admin-auth.service.d.ts.map +1 -1
- package/dist/services/admin-auth.service.js +46 -2
- package/dist/services/admin-auth.service.js.map +1 -1
- package/dist/services/auth-audit.service.d.ts.map +1 -1
- package/dist/services/auth-audit.service.js +1 -1
- package/dist/services/auth-audit.service.js.map +1 -1
- package/dist/services/auth-challenge-helper.service.d.ts.map +1 -1
- package/dist/services/auth-challenge-helper.service.js +34 -0
- package/dist/services/auth-challenge-helper.service.js.map +1 -1
- package/dist/services/auth-service-internal-helpers.d.ts.map +1 -1
- package/dist/services/auth-service-internal-helpers.js +4 -0
- package/dist/services/auth-service-internal-helpers.js.map +1 -1
- package/dist/services/auth.service.d.ts +9 -2
- package/dist/services/auth.service.d.ts.map +1 -1
- package/dist/services/auth.service.js +36 -0
- package/dist/services/auth.service.js.map +1 -1
- package/dist/services/challenge.service.d.ts.map +1 -1
- package/dist/services/challenge.service.js +1 -3
- package/dist/services/challenge.service.js.map +1 -1
- package/dist/services/email-verification.service.d.ts.map +1 -1
- package/dist/services/email-verification.service.js +3 -9
- package/dist/services/email-verification.service.js.map +1 -1
- package/dist/services/mfa-base.service.d.ts +23 -6
- package/dist/services/mfa-base.service.d.ts.map +1 -1
- package/dist/services/mfa-base.service.js +70 -18
- package/dist/services/mfa-base.service.js.map +1 -1
- package/dist/services/mfa.service.d.ts +54 -35
- package/dist/services/mfa.service.d.ts.map +1 -1
- package/dist/services/mfa.service.js +216 -168
- package/dist/services/mfa.service.js.map +1 -1
- package/dist/services/phone-verification.service.d.ts.map +1 -1
- package/dist/services/phone-verification.service.js +3 -8
- package/dist/services/phone-verification.service.js.map +1 -1
- package/dist/services/session.service.d.ts +1 -2
- package/dist/services/session.service.d.ts.map +1 -1
- package/dist/services/session.service.js.map +1 -1
- package/dist/services/user.service.d.ts +5 -5
- package/dist/services/user.service.d.ts.map +1 -1
- package/dist/services/user.service.js +16 -10
- package/dist/services/user.service.js.map +1 -1
- package/package.json +2 -2
- package/dist/dto/admin-remove-devices.dto.d.ts +0 -25
- package/dist/dto/admin-remove-devices.dto.d.ts.map +0 -1
- package/dist/dto/admin-remove-devices.dto.js.map +0 -1
- package/dist/dto/admin-set-preferred-method.dto.d.ts +0 -25
- package/dist/dto/admin-set-preferred-method.dto.d.ts.map +0 -1
- package/dist/dto/admin-set-preferred-method.dto.js +0 -50
- package/dist/dto/admin-set-preferred-method.dto.js.map +0 -1
- package/dist/dto/remove-devices.dto.d.ts +0 -48
- package/dist/dto/remove-devices.dto.d.ts.map +0 -1
- package/dist/dto/remove-devices.dto.js +0 -79
- package/dist/dto/remove-devices.dto.js.map +0 -1
- package/dist/dto/set-preferred-method.dto.d.ts +0 -44
- package/dist/dto/set-preferred-method.dto.d.ts.map +0 -1
- package/dist/dto/set-preferred-method.dto.js +0 -75
- package/dist/dto/set-preferred-method.dto.js.map +0 -1
|
@@ -150,24 +150,28 @@ class MFAService {
|
|
|
150
150
|
* @param methodType - MFA method to remove (normalized)
|
|
151
151
|
* @param removedBy - Actor performing the removal
|
|
152
152
|
*/
|
|
153
|
-
|
|
153
|
+
/**
|
|
154
|
+
* Shared implementation for removing a single MFA device by device ID.
|
|
155
|
+
*
|
|
156
|
+
* @param targetUser - Target user (self-service or admin target)
|
|
157
|
+
* @param deviceId - MFA device ID to remove
|
|
158
|
+
* @param removedBy - Actor performing the removal
|
|
159
|
+
* @returns Removal result
|
|
160
|
+
* @throws {NAuthException} NOT_FOUND when device is not found for the user
|
|
161
|
+
*/
|
|
162
|
+
async removeDeviceInternal(targetUser, deviceId, removedBy) {
|
|
154
163
|
const userId = targetUser.id;
|
|
155
|
-
|
|
156
|
-
const isPreferredMethod = preferredMethod === methodType;
|
|
157
|
-
// Get all active devices for this user
|
|
164
|
+
// Get all active devices for this user and find the target device
|
|
158
165
|
const activeDevices = await this.getActiveDevicesForUserId(userId);
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.VALIDATION_FAILED, `No active ${methodType} MFA devices found for this user`);
|
|
163
|
-
}
|
|
164
|
-
// Delete all devices of this method type
|
|
165
|
-
let deletedCount = 0;
|
|
166
|
-
for (const device of devicesToRemove) {
|
|
167
|
-
const result = await this.mfaDeviceRepository.delete(device.id);
|
|
168
|
-
deletedCount += result.affected || 0;
|
|
166
|
+
const deviceToRemove = activeDevices.find((d) => d.id === deviceId);
|
|
167
|
+
if (!deviceToRemove) {
|
|
168
|
+
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'MFA device not found', { deviceId });
|
|
169
169
|
}
|
|
170
|
-
|
|
170
|
+
const removedMethod = deviceToRemove.type;
|
|
171
|
+
// Delete the specific device
|
|
172
|
+
const result = await this.mfaDeviceRepository.delete(deviceId);
|
|
173
|
+
const deletedCount = result.affected || 0;
|
|
174
|
+
// Re-load remaining devices after deletion
|
|
171
175
|
const remainingActiveDevices = await this.getActiveDevicesForUserId(userId);
|
|
172
176
|
let mfaDisabled = false;
|
|
173
177
|
// If no active devices remain, disable MFA for user
|
|
@@ -179,23 +183,30 @@ class MFAService {
|
|
|
179
183
|
});
|
|
180
184
|
mfaDisabled = true;
|
|
181
185
|
// ============================================================================
|
|
182
|
-
// Audit: Record MFA disabled (
|
|
186
|
+
// Audit: Record MFA disabled (last device removed)
|
|
183
187
|
// ============================================================================
|
|
184
188
|
if (this.auditService && this.clientInfoService) {
|
|
185
189
|
try {
|
|
190
|
+
const actor = removedBy === 'admin' ? context_storage_1.ContextStorage.get('CURRENT_USER') : null;
|
|
191
|
+
const actorNameCandidate = actor
|
|
192
|
+
? `${actor.firstName || ''} ${actor.lastName || ''}`.trim()
|
|
193
|
+
: '';
|
|
194
|
+
const performedByName = actorNameCandidate.length > 0 ? actorNameCandidate : actor?.email || undefined;
|
|
186
195
|
await this.auditService?.recordEvent({
|
|
187
196
|
userId,
|
|
188
197
|
eventType: auth_audit_event_type_enum_1.AuthAuditEventType.MFA_DISABLED,
|
|
189
198
|
eventStatus: 'INFO',
|
|
190
|
-
|
|
199
|
+
authMethod: removedBy === 'admin' ? 'admin' : undefined,
|
|
200
|
+
performedBy: removedBy === 'admin' && actor ? actor.sub : undefined,
|
|
201
|
+
reason: removedBy === 'admin' ? 'admin_action' : 'last_device_removed',
|
|
191
202
|
description: removedBy === 'admin'
|
|
192
|
-
? 'MFA disabled by admin -
|
|
193
|
-
: 'MFA disabled -
|
|
194
|
-
// Client info automatically included from context
|
|
203
|
+
? 'MFA disabled by admin - last device removed'
|
|
204
|
+
: 'MFA disabled - last device removed',
|
|
195
205
|
metadata: {
|
|
196
|
-
|
|
197
|
-
|
|
206
|
+
removedDeviceId: deviceId,
|
|
207
|
+
removedMethod,
|
|
198
208
|
removedBy,
|
|
209
|
+
...(performedByName ? { performedByName } : {}),
|
|
199
210
|
},
|
|
200
211
|
});
|
|
201
212
|
}
|
|
@@ -216,11 +227,11 @@ class MFAService {
|
|
|
216
227
|
allowedMethods: this.config.mfa.allowedMethods || [],
|
|
217
228
|
requiresSetup: true,
|
|
218
229
|
});
|
|
219
|
-
this.logger?.log?.(`Created MFA_SETUP_REQUIRED challenge for user ${targetUser.sub} after
|
|
230
|
+
this.logger?.log?.(`Created MFA_SETUP_REQUIRED challenge for user ${targetUser.sub} after device removal`);
|
|
220
231
|
}
|
|
221
232
|
catch (error) {
|
|
222
233
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
223
|
-
this.logger?.warn?.(`Failed to create MFA_SETUP_REQUIRED challenge after
|
|
234
|
+
this.logger?.warn?.(`Failed to create MFA_SETUP_REQUIRED challenge after device removal: ${errorMessage}`);
|
|
224
235
|
}
|
|
225
236
|
}
|
|
226
237
|
}
|
|
@@ -228,30 +239,19 @@ class MFAService {
|
|
|
228
239
|
else {
|
|
229
240
|
// Update mfaMethods array with remaining methods
|
|
230
241
|
const remainingMethods = [...new Set(remainingActiveDevices.map((d) => d.type))];
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if (remainingActiveDevices[i].id) {
|
|
245
|
-
await this.mfaDeviceRepository.update({ id: remainingActiveDevices[i].id }, { isPrimary: false });
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
this.logger?.log?.(`Updated preferred MFA method to ${newPreferredMethod} after removing ${methodType}`);
|
|
249
|
-
}
|
|
250
|
-
else {
|
|
251
|
-
// No preferred method change needed, just update mfaMethods
|
|
252
|
-
await this.userRepository.update({ id: userId }, {
|
|
253
|
-
mfaMethods: remainingMethods,
|
|
254
|
-
});
|
|
242
|
+
// Determine preferred method: keep if still configured, otherwise pick a new one.
|
|
243
|
+
const preferredMethod = targetUser.preferredMfaMethod;
|
|
244
|
+
const preferredStillConfigured = !!preferredMethod && remainingActiveDevices.some((d) => d.type === preferredMethod);
|
|
245
|
+
const finalPreferredMethod = preferredStillConfigured ? preferredMethod : remainingActiveDevices[0].type;
|
|
246
|
+
await this.userRepository.update({ id: userId }, {
|
|
247
|
+
mfaMethods: remainingMethods,
|
|
248
|
+
preferredMfaMethod: finalPreferredMethod,
|
|
249
|
+
});
|
|
250
|
+
// Normalize primary device flags: ensure exactly one active device is primary.
|
|
251
|
+
// Prefer a device of the preferred method (best UX).
|
|
252
|
+
const primaryDevice = remainingActiveDevices.find((d) => d.type === finalPreferredMethod) || remainingActiveDevices[0];
|
|
253
|
+
for (const device of remainingActiveDevices) {
|
|
254
|
+
await this.mfaDeviceRepository.update({ id: device.id }, { isPrimary: device.id === primaryDevice.id });
|
|
255
255
|
}
|
|
256
256
|
}
|
|
257
257
|
// ============================================================================
|
|
@@ -259,18 +259,26 @@ class MFAService {
|
|
|
259
259
|
// ============================================================================
|
|
260
260
|
if (deletedCount > 0 && this.auditService && this.clientInfoService) {
|
|
261
261
|
try {
|
|
262
|
+
const actor = removedBy === 'admin' ? context_storage_1.ContextStorage.get('CURRENT_USER') : null;
|
|
263
|
+
const actorNameCandidate = actor
|
|
264
|
+
? `${actor.firstName || ''} ${actor.lastName || ''}`.trim()
|
|
265
|
+
: '';
|
|
266
|
+
const performedByName = actorNameCandidate.length > 0 ? actorNameCandidate : actor?.email || undefined;
|
|
262
267
|
await this.auditService?.recordEvent({
|
|
263
268
|
userId,
|
|
264
269
|
eventType: auth_audit_event_type_enum_1.AuthAuditEventType.MFA_DEVICE_REMOVED,
|
|
265
270
|
eventStatus: 'INFO',
|
|
271
|
+
authMethod: removedBy === 'admin' ? 'admin' : undefined,
|
|
272
|
+
performedBy: removedBy === 'admin' && actor ? actor.sub : undefined,
|
|
266
273
|
metadata: {
|
|
267
|
-
|
|
274
|
+
removedDeviceId: deviceId,
|
|
275
|
+
removedMethod,
|
|
268
276
|
deletedCount,
|
|
269
277
|
remainingDevices: remainingActiveDevices.length,
|
|
270
278
|
mfaDisabled,
|
|
271
279
|
removedBy,
|
|
280
|
+
...(performedByName ? { performedByName } : {}),
|
|
272
281
|
},
|
|
273
|
-
// Client info automatically included from context
|
|
274
282
|
});
|
|
275
283
|
}
|
|
276
284
|
catch (auditError) {
|
|
@@ -278,7 +286,7 @@ class MFAService {
|
|
|
278
286
|
this.logger?.error?.(`Failed to record MFA_DEVICE_REMOVED audit event: ${errorMessage}`, {
|
|
279
287
|
error: auditError,
|
|
280
288
|
userId,
|
|
281
|
-
|
|
289
|
+
removedDeviceId: deviceId,
|
|
282
290
|
});
|
|
283
291
|
}
|
|
284
292
|
}
|
|
@@ -290,7 +298,7 @@ class MFAService {
|
|
|
290
298
|
const clientInfo = this.clientInfoService.get();
|
|
291
299
|
await this.hookRegistry.executeMFADeviceRemoved({
|
|
292
300
|
user: targetUser,
|
|
293
|
-
deviceType:
|
|
301
|
+
deviceType: removedMethod,
|
|
294
302
|
removedBy,
|
|
295
303
|
remainingDeviceCount: remainingActiveDevices.length,
|
|
296
304
|
clientInfo: {
|
|
@@ -306,64 +314,14 @@ class MFAService {
|
|
|
306
314
|
this.logger?.error?.(`Failed to execute mfaDeviceRemoved hooks: ${errorMessage}`, {
|
|
307
315
|
error: hookError,
|
|
308
316
|
userId,
|
|
309
|
-
|
|
310
|
-
});
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
return { deletedCount, mfaDisabled };
|
|
314
|
-
}
|
|
315
|
-
/**
|
|
316
|
-
* Shared implementation for setting preferred MFA method.
|
|
317
|
-
*
|
|
318
|
-
* @param targetUser - Target user (self-service or admin target)
|
|
319
|
-
* @param methodType - Preferred method (normalized)
|
|
320
|
-
* @param updatedBy - Actor performing the update
|
|
321
|
-
*/
|
|
322
|
-
async setPreferredMethodInternal(targetUser, methodType, updatedBy) {
|
|
323
|
-
// Verify user has this method configured
|
|
324
|
-
const activeDevices = await this.getActiveDevicesForUserId(targetUser.id);
|
|
325
|
-
const preferredDevice = activeDevices.find((d) => d.type.toLowerCase() === methodType && d.isActive);
|
|
326
|
-
if (!preferredDevice) {
|
|
327
|
-
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.VALIDATION_FAILED, `MFA method '${methodType}' is not configured for this user`);
|
|
328
|
-
}
|
|
329
|
-
// Update user's preferred method
|
|
330
|
-
await this.userRepository.update({ id: targetUser.id }, {
|
|
331
|
-
preferredMfaMethod: methodType,
|
|
332
|
-
});
|
|
333
|
-
// Update device isPrimary flags: set preferred device as primary, unset others
|
|
334
|
-
for (const device of activeDevices) {
|
|
335
|
-
await this.mfaDeviceRepository.update({ id: device.id }, { isPrimary: device.id === preferredDevice.id });
|
|
336
|
-
}
|
|
337
|
-
this.logger?.log?.(`Device ${preferredDevice.id} set as primary for user ${targetUser.sub} (by ${updatedBy})`);
|
|
338
|
-
// ============================================================================
|
|
339
|
-
// Audit: Record preferred MFA method update
|
|
340
|
-
// ============================================================================
|
|
341
|
-
if (this.auditService && this.clientInfoService) {
|
|
342
|
-
try {
|
|
343
|
-
const previousMethod = targetUser.preferredMfaMethod;
|
|
344
|
-
await this.auditService?.recordEvent({
|
|
345
|
-
userId: targetUser.id,
|
|
346
|
-
eventType: auth_audit_event_type_enum_1.AuthAuditEventType.MFA_PREFERRED_METHOD_UPDATED,
|
|
347
|
-
eventStatus: 'INFO',
|
|
348
|
-
metadata: {
|
|
349
|
-
previousMethod: previousMethod || null,
|
|
350
|
-
newMethod: methodType,
|
|
351
|
-
deviceId: preferredDevice.id,
|
|
352
|
-
updatedBy,
|
|
353
|
-
},
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
catch (auditError) {
|
|
357
|
-
const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
|
|
358
|
-
this.logger?.error?.(`Failed to record MFA_PREFERRED_METHOD_UPDATED audit event: ${errorMessage}`, {
|
|
359
|
-
error: auditError,
|
|
360
|
-
userId: targetUser.id,
|
|
361
|
-
method: methodType,
|
|
317
|
+
removedDeviceId: deviceId,
|
|
362
318
|
});
|
|
363
319
|
}
|
|
364
320
|
}
|
|
365
321
|
return {
|
|
366
|
-
|
|
322
|
+
removedDeviceId: deviceId,
|
|
323
|
+
removedMethod,
|
|
324
|
+
mfaDisabled,
|
|
367
325
|
};
|
|
368
326
|
}
|
|
369
327
|
/**
|
|
@@ -672,8 +630,9 @@ class MFAService {
|
|
|
672
630
|
await (0, dto_validator_1.ensureValidatedDto)(dto_1.GetUserDevicesDTO, _dto);
|
|
673
631
|
// Get user from authenticated context (already has id and sub)
|
|
674
632
|
const currentUser = this.getCurrentUserOrThrow();
|
|
633
|
+
const devices = await this.getActiveDevicesForUserId(currentUser.id);
|
|
675
634
|
return {
|
|
676
|
-
devices:
|
|
635
|
+
devices: dto_1.MFADeviceResponseDTO.fromEntities(devices),
|
|
677
636
|
};
|
|
678
637
|
}
|
|
679
638
|
/**
|
|
@@ -724,94 +683,179 @@ class MFAService {
|
|
|
724
683
|
* }
|
|
725
684
|
* ```
|
|
726
685
|
*/
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
686
|
+
/**
|
|
687
|
+
* Remove a single MFA device by device ID (self-service).
|
|
688
|
+
*
|
|
689
|
+
* WHY: Users can register multiple devices per method (e.g., multiple passkeys, multiple TOTP apps),
|
|
690
|
+
* so deleting by method alone is often too destructive.
|
|
691
|
+
*
|
|
692
|
+
* @param dto - Request DTO with deviceId
|
|
693
|
+
* @returns Removal response (removed device id/method and whether MFA was disabled)
|
|
694
|
+
* @throws {NAuthException} NOT_FOUND when device does not exist or does not belong to the user
|
|
695
|
+
*
|
|
696
|
+
* @example
|
|
697
|
+
* ```typescript
|
|
698
|
+
* const result = await mfaService.removeDevice({ deviceId: 123 });
|
|
699
|
+
* ```
|
|
700
|
+
*/
|
|
701
|
+
async removeDevice(dto) {
|
|
702
|
+
dto = await (0, dto_validator_1.ensureValidatedDto)(dto_1.RemoveDeviceDTO, dto);
|
|
734
703
|
const currentUser = this.getCurrentUserOrThrow();
|
|
735
|
-
return await this.
|
|
704
|
+
return await this.removeDeviceInternal(currentUser, dto.deviceId, 'user');
|
|
736
705
|
}
|
|
737
706
|
/**
|
|
738
|
-
* Admin: Remove
|
|
707
|
+
* Admin: Remove a single MFA device by device ID.
|
|
739
708
|
*
|
|
740
|
-
*
|
|
741
|
-
*
|
|
742
|
-
*
|
|
743
|
-
* @
|
|
709
|
+
* Admin APIs are allowed to target any user's device. The owning user is resolved
|
|
710
|
+
* from the device record and the same internal removal logic is reused.
|
|
711
|
+
*
|
|
712
|
+
* @param dto - Admin request DTO with deviceId
|
|
713
|
+
* @returns Removal response
|
|
714
|
+
* @throws {NAuthException} NOT_FOUND when device or owning user is not found
|
|
744
715
|
*
|
|
745
716
|
* @example
|
|
746
717
|
* ```typescript
|
|
747
|
-
* await mfaService.
|
|
718
|
+
* const result = await mfaService.adminRemoveDevice({ deviceId: 123 });
|
|
748
719
|
* ```
|
|
749
720
|
*/
|
|
750
|
-
async
|
|
751
|
-
dto = await (0, dto_validator_1.ensureValidatedDto)(dto_1.
|
|
752
|
-
const
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
721
|
+
async adminRemoveDevice(dto) {
|
|
722
|
+
dto = await (0, dto_validator_1.ensureValidatedDto)(dto_1.AdminRemoveDeviceDTO, dto);
|
|
723
|
+
const deviceEntity = await this.mfaDeviceRepository.findOne({
|
|
724
|
+
where: { id: dto.deviceId },
|
|
725
|
+
});
|
|
726
|
+
if (!deviceEntity) {
|
|
727
|
+
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'MFA device not found', { deviceId: dto.deviceId });
|
|
756
728
|
}
|
|
757
|
-
const
|
|
758
|
-
|
|
729
|
+
const device = deviceEntity;
|
|
730
|
+
const userEntity = await this.userRepository.findOne({
|
|
731
|
+
where: { id: device.userId },
|
|
732
|
+
});
|
|
733
|
+
if (!userEntity) {
|
|
734
|
+
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found', {
|
|
735
|
+
userId: device.userId,
|
|
736
|
+
deviceId: dto.deviceId,
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
const targetUser = userEntity;
|
|
740
|
+
return await this.removeDeviceInternal(targetUser, dto.deviceId, 'admin');
|
|
759
741
|
}
|
|
760
742
|
/**
|
|
761
|
-
* Set preferred MFA
|
|
743
|
+
* Admin: Set a user's preferred MFA device.
|
|
762
744
|
*
|
|
763
|
-
* Updates the
|
|
764
|
-
*
|
|
745
|
+
* Updates the preferred device for a specified user by device ID.
|
|
746
|
+
* This is an admin-only operation that allows administrators to manage
|
|
747
|
+
* user MFA preferences.
|
|
765
748
|
*
|
|
766
|
-
*
|
|
767
|
-
*
|
|
768
|
-
*
|
|
769
|
-
* @param dto - Request DTO with method type
|
|
770
|
-
* @returns Response DTO with success message
|
|
771
|
-
* @throws {NAuthException} If user not found, invalid method type, or method not configured
|
|
749
|
+
* @param dto - DTO containing user sub and device ID
|
|
750
|
+
* @returns Success message
|
|
751
|
+
* @throws {NAuthException} NOT_FOUND | VALIDATION_FAILED
|
|
772
752
|
*
|
|
773
753
|
* @example
|
|
774
754
|
* ```typescript
|
|
775
|
-
*
|
|
776
|
-
*
|
|
777
|
-
*
|
|
778
|
-
*
|
|
779
|
-
* }
|
|
755
|
+
* const result = await mfaService.adminSetPreferredDevice({
|
|
756
|
+
* sub: 'user-uuid',
|
|
757
|
+
* deviceId: 123
|
|
758
|
+
* });
|
|
759
|
+
* // Returns: { message: "Preferred MFA device updated" }
|
|
780
760
|
* ```
|
|
781
761
|
*/
|
|
782
|
-
async
|
|
783
|
-
dto = await (0, dto_validator_1.ensureValidatedDto)(dto_1.
|
|
784
|
-
//
|
|
785
|
-
const
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
762
|
+
async adminSetPreferredDevice(dto) {
|
|
763
|
+
dto = await (0, dto_validator_1.ensureValidatedDto)(dto_1.AdminSetPreferredDeviceDTO, dto);
|
|
764
|
+
// Get target user
|
|
765
|
+
const user = await this.userRepository.findOne({ where: { sub: dto.sub } });
|
|
766
|
+
if (!user) {
|
|
767
|
+
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'User not found');
|
|
768
|
+
}
|
|
769
|
+
// Delegate to the regular setPreferredDevice logic
|
|
770
|
+
// but use SetPreferredDeviceDTO format
|
|
771
|
+
const deviceDto = Object.assign(new dto_1.SetPreferredDeviceDTO(), { deviceId: dto.deviceId });
|
|
772
|
+
// Temporarily set context user for the operation
|
|
773
|
+
// Keep admin user in context for audit trails
|
|
774
|
+
const originalUser = context_storage_1.ContextStorage.get('CURRENT_USER');
|
|
775
|
+
const adminUser = originalUser; // Admin user is already in context
|
|
776
|
+
// Set target user as current user for the operation
|
|
777
|
+
context_storage_1.ContextStorage.set('CURRENT_USER', user);
|
|
778
|
+
try {
|
|
779
|
+
// Pass 'admin' flag to setPreferredDevice for proper audit trails
|
|
780
|
+
const result = await this.setPreferredDevice(deviceDto, 'admin');
|
|
781
|
+
return { message: result.message };
|
|
782
|
+
}
|
|
783
|
+
finally {
|
|
784
|
+
// Restore original context (admin user)
|
|
785
|
+
if (originalUser) {
|
|
786
|
+
context_storage_1.ContextStorage.set('CURRENT_USER', originalUser);
|
|
787
|
+
}
|
|
788
|
+
else {
|
|
789
|
+
context_storage_1.ContextStorage.set('CURRENT_USER', undefined);
|
|
790
|
+
}
|
|
789
791
|
}
|
|
790
|
-
const currentUser = this.getCurrentUserOrThrow();
|
|
791
|
-
return await this.setPreferredMethodInternal(currentUser, normalizedMethod, 'user');
|
|
792
792
|
}
|
|
793
793
|
/**
|
|
794
|
-
*
|
|
794
|
+
* Set a specific MFA device as the user's preferred device (self-service).
|
|
795
795
|
*
|
|
796
|
-
*
|
|
797
|
-
*
|
|
798
|
-
*
|
|
799
|
-
*
|
|
796
|
+
* This updates:
|
|
797
|
+
* - The user's preferred MFA method (set to the device's method)
|
|
798
|
+
* - Device `isPrimary` flags (exactly one active device becomes primary/preferred)
|
|
799
|
+
*
|
|
800
|
+
* @param dto - Request DTO with deviceId
|
|
801
|
+
* @returns Success message
|
|
802
|
+
* @throws {NAuthException} NOT_FOUND when device does not exist or does not belong to the user
|
|
800
803
|
*
|
|
801
804
|
* @example
|
|
802
805
|
* ```typescript
|
|
803
|
-
* await mfaService.
|
|
806
|
+
* await mfaService.setPreferredDevice({ deviceId: 123 });
|
|
804
807
|
* ```
|
|
805
808
|
*/
|
|
806
|
-
async
|
|
807
|
-
dto = await (0, dto_validator_1.ensureValidatedDto)(dto_1.
|
|
808
|
-
const
|
|
809
|
-
const
|
|
810
|
-
|
|
811
|
-
|
|
809
|
+
async setPreferredDevice(dto, updatedBy = 'user') {
|
|
810
|
+
dto = await (0, dto_validator_1.ensureValidatedDto)(dto_1.SetPreferredDeviceDTO, dto);
|
|
811
|
+
const currentUser = this.getCurrentUserOrThrow();
|
|
812
|
+
const activeDevices = await this.getActiveDevicesForUserId(currentUser.id);
|
|
813
|
+
const targetDevice = activeDevices.find((d) => d.id === dto.deviceId);
|
|
814
|
+
if (!targetDevice) {
|
|
815
|
+
throw new nauth_exception_1.NAuthException(error_codes_enum_1.AuthErrorCode.NOT_FOUND, 'MFA device not found', { deviceId: dto.deviceId });
|
|
816
|
+
}
|
|
817
|
+
// Update user's preferred method to match the preferred device's method.
|
|
818
|
+
await this.userRepository.update({ id: currentUser.id }, {
|
|
819
|
+
preferredMfaMethod: targetDevice.type,
|
|
820
|
+
});
|
|
821
|
+
// Ensure exactly one active device is primary (marks it as preferred).
|
|
822
|
+
for (const device of activeDevices) {
|
|
823
|
+
await this.mfaDeviceRepository.update({ id: device.id }, { isPrimary: device.id === targetDevice.id });
|
|
824
|
+
}
|
|
825
|
+
// ============================================================================
|
|
826
|
+
// Audit: Record preferred MFA device update
|
|
827
|
+
// ============================================================================
|
|
828
|
+
if (this.auditService && this.clientInfoService) {
|
|
829
|
+
try {
|
|
830
|
+
const actor = updatedBy === 'admin' ? context_storage_1.ContextStorage.get('CURRENT_USER') : currentUser;
|
|
831
|
+
const actorNameCandidate = actor && updatedBy === 'admin'
|
|
832
|
+
? `${actor.firstName || ''} ${actor.lastName || ''}`.trim()
|
|
833
|
+
: '';
|
|
834
|
+
const performedByName = actorNameCandidate.length > 0 ? actorNameCandidate : actor?.email || undefined;
|
|
835
|
+
await this.auditService?.recordEvent({
|
|
836
|
+
userId: currentUser.id,
|
|
837
|
+
eventType: auth_audit_event_type_enum_1.AuthAuditEventType.MFA_PREFERRED_METHOD_UPDATED,
|
|
838
|
+
eventStatus: 'INFO',
|
|
839
|
+
authMethod: updatedBy === 'admin' ? 'admin' : undefined,
|
|
840
|
+
performedBy: updatedBy === 'admin' && actor && actor.sub !== currentUser.sub ? actor.sub : undefined,
|
|
841
|
+
metadata: {
|
|
842
|
+
newMethod: targetDevice.type,
|
|
843
|
+
deviceId: targetDevice.id,
|
|
844
|
+
updatedBy,
|
|
845
|
+
...(performedByName && updatedBy === 'admin' ? { performedByName } : {}),
|
|
846
|
+
},
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
catch (auditError) {
|
|
850
|
+
const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
|
|
851
|
+
this.logger?.error?.(`Failed to record MFA_PREFERRED_METHOD_UPDATED audit event: ${errorMessage}`, {
|
|
852
|
+
error: auditError,
|
|
853
|
+
userId: currentUser.id,
|
|
854
|
+
deviceId: targetDevice.id,
|
|
855
|
+
});
|
|
856
|
+
}
|
|
812
857
|
}
|
|
813
|
-
|
|
814
|
-
return await this.setPreferredMethodInternal(targetUser, normalizedMethod, 'admin');
|
|
858
|
+
return { message: 'Preferred MFA device updated' };
|
|
815
859
|
}
|
|
816
860
|
/**
|
|
817
861
|
* Grant or revoke a user's exemption from multi-factor authentication (MFA) requirements.
|
|
@@ -879,9 +923,13 @@ class MFAService {
|
|
|
879
923
|
// ============================================================================
|
|
880
924
|
// Audit: Record MFA exemption grant/revoke
|
|
881
925
|
// ============================================================================
|
|
882
|
-
// Note:
|
|
926
|
+
// Note: We also attach performedByName when possible so downstream systems
|
|
927
|
+
// have a stable snapshot even if admin profile changes later.
|
|
883
928
|
if (this.auditService && this.clientInfoService) {
|
|
884
929
|
try {
|
|
930
|
+
const actor = context_storage_1.ContextStorage.get('CURRENT_USER');
|
|
931
|
+
const actorNameCandidate = `${actor?.firstName || ''} ${actor?.lastName || ''}`.trim();
|
|
932
|
+
const performedByName = actorNameCandidate.length > 0 ? actorNameCandidate : actor?.email || undefined;
|
|
885
933
|
await this.auditService.recordEvent({
|
|
886
934
|
userId: user.id,
|
|
887
935
|
eventType: dto.exempt ? auth_audit_event_type_enum_1.AuthAuditEventType.MFA_EXEMPTION_GRANTED : auth_audit_event_type_enum_1.AuthAuditEventType.MFA_EXEMPTION_REVOKED,
|
|
@@ -892,7 +940,7 @@ class MFAService {
|
|
|
892
940
|
metadata: {
|
|
893
941
|
previousExemptStatus: userEntity.mfaExempt,
|
|
894
942
|
newExemptStatus: dto.exempt,
|
|
895
|
-
|
|
943
|
+
...(performedByName ? { performedByName } : {}),
|
|
896
944
|
},
|
|
897
945
|
});
|
|
898
946
|
}
|