@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.
- package/dist/dto/admin-get-user-auth-history.dto.d.ts +62 -0
- package/dist/dto/admin-get-user-auth-history.dto.d.ts.map +1 -0
- package/dist/dto/admin-get-user-auth-history.dto.js +87 -0
- package/dist/dto/admin-get-user-auth-history.dto.js.map +1 -0
- package/dist/dto/admin-logout-all.dto.d.ts +48 -0
- package/dist/dto/admin-logout-all.dto.d.ts.map +1 -0
- package/dist/dto/{change-password-request.dto.js → admin-logout-all.dto.js} +36 -21
- package/dist/dto/admin-logout-all.dto.js.map +1 -0
- package/dist/dto/admin-remove-devices.dto.d.ts +25 -0
- package/dist/dto/admin-remove-devices.dto.d.ts.map +1 -0
- package/dist/dto/admin-remove-devices.dto.js +50 -0
- package/dist/dto/admin-remove-devices.dto.js.map +1 -0
- package/dist/dto/admin-reset-password.dto.d.ts +15 -19
- package/dist/dto/admin-reset-password.dto.d.ts.map +1 -1
- package/dist/dto/admin-reset-password.dto.js +21 -41
- package/dist/dto/admin-reset-password.dto.js.map +1 -1
- package/dist/dto/admin-revoke-session.dto.d.ts +22 -0
- package/dist/dto/admin-revoke-session.dto.d.ts.map +1 -0
- package/dist/dto/admin-revoke-session.dto.js +48 -0
- package/dist/dto/admin-revoke-session.dto.js.map +1 -0
- package/dist/dto/admin-set-password.dto.d.ts +8 -10
- package/dist/dto/admin-set-password.dto.d.ts.map +1 -1
- package/dist/dto/admin-set-password.dto.js +11 -21
- package/dist/dto/admin-set-password.dto.js.map +1 -1
- package/dist/dto/admin-set-preferred-method.dto.d.ts +25 -0
- package/dist/dto/admin-set-preferred-method.dto.d.ts.map +1 -0
- package/dist/dto/admin-set-preferred-method.dto.js +50 -0
- package/dist/dto/admin-set-preferred-method.dto.js.map +1 -0
- package/dist/dto/admin-update-user-attributes.dto.d.ts +41 -0
- package/dist/dto/admin-update-user-attributes.dto.d.ts.map +1 -0
- package/dist/dto/{update-user-attributes-request.dto.js → admin-update-user-attributes.dto.js} +12 -17
- package/dist/dto/admin-update-user-attributes.dto.js.map +1 -0
- package/dist/dto/auth-challenge.dto.d.ts +2 -2
- package/dist/dto/auth-challenge.dto.d.ts.map +1 -1
- package/dist/dto/auth-challenge.dto.js +3 -3
- package/dist/dto/auth-challenge.dto.js.map +1 -1
- package/dist/dto/auth-response.dto.d.ts +1 -1
- package/dist/dto/auth-response.dto.d.ts.map +1 -1
- package/dist/dto/auth-response.dto.js +1 -1
- package/dist/dto/auth-response.dto.js.map +1 -1
- package/dist/dto/get-mfa-status.dto.d.ts +8 -4
- package/dist/dto/get-mfa-status.dto.d.ts.map +1 -1
- package/dist/dto/get-mfa-status.dto.js +8 -4
- package/dist/dto/get-mfa-status.dto.js.map +1 -1
- package/dist/dto/get-risk-assessment-history.dto.d.ts +3 -3
- package/dist/dto/get-risk-assessment-history.dto.d.ts.map +1 -1
- package/dist/dto/get-risk-assessment-history.dto.js +5 -5
- package/dist/dto/get-risk-assessment-history.dto.js.map +1 -1
- package/dist/dto/get-suspicious-activity.dto.d.ts +3 -3
- package/dist/dto/get-suspicious-activity.dto.d.ts.map +1 -1
- package/dist/dto/get-suspicious-activity.dto.js +5 -5
- package/dist/dto/get-suspicious-activity.dto.js.map +1 -1
- package/dist/dto/get-user-auth-history.dto.d.ts +4 -39
- package/dist/dto/get-user-auth-history.dto.d.ts.map +1 -1
- package/dist/dto/get-user-auth-history.dto.js +53 -51
- package/dist/dto/get-user-auth-history.dto.js.map +1 -1
- package/dist/dto/get-user-devices.dto.d.ts +5 -18
- package/dist/dto/get-user-devices.dto.d.ts.map +1 -1
- package/dist/dto/get-user-devices.dto.js +5 -39
- package/dist/dto/get-user-devices.dto.js.map +1 -1
- package/dist/dto/get-user-sessions-response.dto.d.ts +1 -1
- package/dist/dto/get-user-sessions-response.dto.js +1 -1
- package/dist/dto/get-user-sessions.dto.d.ts +1 -1
- package/dist/dto/get-user-sessions.dto.js +1 -1
- package/dist/dto/index.d.ts +8 -2
- package/dist/dto/index.d.ts.map +1 -1
- package/dist/dto/index.js +8 -2
- package/dist/dto/index.js.map +1 -1
- package/dist/dto/logout-all-response.dto.d.ts +1 -1
- package/dist/dto/logout-all-response.dto.js +1 -1
- package/dist/dto/logout-all.dto.d.ts +1 -18
- package/dist/dto/logout-all.dto.d.ts.map +1 -1
- package/dist/dto/logout-all.dto.js +1 -30
- package/dist/dto/logout-all.dto.js.map +1 -1
- package/dist/dto/logout-session.dto.d.ts +0 -5
- package/dist/dto/logout-session.dto.d.ts.map +1 -1
- package/dist/dto/logout-session.dto.js +0 -12
- package/dist/dto/logout-session.dto.js.map +1 -1
- package/dist/dto/logout.dto.d.ts +1 -18
- package/dist/dto/logout.dto.d.ts.map +1 -1
- package/dist/dto/logout.dto.js +1 -30
- package/dist/dto/logout.dto.js.map +1 -1
- package/dist/dto/remove-devices.dto.d.ts +4 -16
- package/dist/dto/remove-devices.dto.d.ts.map +1 -1
- package/dist/dto/remove-devices.dto.js +4 -26
- package/dist/dto/remove-devices.dto.js.map +1 -1
- package/dist/dto/set-mfa-exemption.dto.d.ts +8 -9
- package/dist/dto/set-mfa-exemption.dto.d.ts.map +1 -1
- package/dist/dto/set-mfa-exemption.dto.js +11 -13
- package/dist/dto/set-mfa-exemption.dto.js.map +1 -1
- package/dist/dto/set-must-change-password.dto.d.ts +3 -3
- package/dist/dto/set-must-change-password.dto.d.ts.map +1 -1
- package/dist/dto/set-must-change-password.dto.js +5 -5
- package/dist/dto/set-must-change-password.dto.js.map +1 -1
- package/dist/dto/set-preferred-method.dto.d.ts +4 -16
- package/dist/dto/set-preferred-method.dto.d.ts.map +1 -1
- package/dist/dto/set-preferred-method.dto.js +4 -26
- package/dist/dto/set-preferred-method.dto.js.map +1 -1
- package/dist/dto/setup-mfa.dto.d.ts +3 -18
- package/dist/dto/setup-mfa.dto.d.ts.map +1 -1
- package/dist/dto/setup-mfa.dto.js +3 -30
- package/dist/dto/setup-mfa.dto.js.map +1 -1
- package/dist/dto/social-auth.dto.d.ts +4 -34
- package/dist/dto/social-auth.dto.d.ts.map +1 -1
- package/dist/dto/social-auth.dto.js +10 -68
- package/dist/dto/social-auth.dto.js.map +1 -1
- package/dist/dto/update-user-attributes.dto.d.ts +26 -0
- package/dist/dto/update-user-attributes.dto.d.ts.map +1 -0
- package/dist/dto/update-user-attributes.dto.js +30 -0
- package/dist/dto/update-user-attributes.dto.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/interfaces/hooks.interface.d.ts +2 -1
- package/dist/interfaces/hooks.interface.d.ts.map +1 -1
- package/dist/interfaces/provider.interface.d.ts +1 -1
- package/dist/interfaces/provider.interface.d.ts.map +1 -1
- package/dist/services/adaptive-mfa-decision.service.js +2 -2
- package/dist/services/adaptive-mfa-decision.service.js.map +1 -1
- package/dist/services/admin-auth.service.d.ts +307 -0
- package/dist/services/admin-auth.service.d.ts.map +1 -0
- package/dist/services/admin-auth.service.js +885 -0
- package/dist/services/admin-auth.service.js.map +1 -0
- package/dist/services/auth-audit.service.d.ts +16 -16
- package/dist/services/auth-audit.service.d.ts.map +1 -1
- package/dist/services/auth-audit.service.js +33 -33
- package/dist/services/auth-audit.service.js.map +1 -1
- package/dist/services/auth-challenge-helper.service.js +3 -3
- package/dist/services/auth-challenge-helper.service.js.map +1 -1
- package/dist/services/auth-service-internal-helpers.d.ts +2 -2
- package/dist/services/auth-service-internal-helpers.d.ts.map +1 -1
- package/dist/services/auth-service-internal-helpers.js.map +1 -1
- package/dist/services/auth.service.d.ts +122 -438
- package/dist/services/auth.service.d.ts.map +1 -1
- package/dist/services/auth.service.js +424 -1255
- package/dist/services/auth.service.js.map +1 -1
- package/dist/services/mfa.service.d.ts +80 -12
- package/dist/services/mfa.service.d.ts.map +1 -1
- package/dist/services/mfa.service.js +347 -261
- package/dist/services/mfa.service.js.map +1 -1
- package/dist/services/social-auth.service.d.ts +7 -0
- package/dist/services/social-auth.service.d.ts.map +1 -1
- package/dist/services/social-auth.service.js +38 -26
- package/dist/services/social-auth.service.js.map +1 -1
- package/dist/services/user.service.d.ts +3 -3
- package/dist/services/user.service.d.ts.map +1 -1
- package/dist/services/user.service.js +7 -7
- package/dist/services/user.service.js.map +1 -1
- package/dist/utils/dto-validator.d.ts.map +1 -1
- package/dist/utils/dto-validator.js +50 -4
- package/dist/utils/dto-validator.js.map +1 -1
- package/dist/utils/setup/init-services.d.ts +2 -1
- package/dist/utils/setup/init-services.d.ts.map +1 -1
- package/dist/utils/setup/init-services.js +2 -0
- package/dist/utils/setup/init-services.js.map +1 -1
- package/package.json +1 -1
- package/dist/dto/change-password-request.dto.d.ts +0 -43
- package/dist/dto/change-password-request.dto.d.ts.map +0 -1
- package/dist/dto/change-password-request.dto.js.map +0 -1
- package/dist/dto/update-user-attributes-request.dto.d.ts +0 -44
- package/dist/dto/update-user-attributes-request.dto.d.ts.map +0 -1
- 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
|
-
//
|
|
286
|
-
const
|
|
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
|
-
*
|
|
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(
|
|
572
|
+
* const result = await this.mfaService.getUserDevices();
|
|
306
573
|
* // Returns: { devices: [...] }
|
|
307
574
|
* ```
|
|
308
575
|
*/
|
|
309
|
-
async getUserDevices(
|
|
310
|
-
|
|
311
|
-
//
|
|
312
|
-
const
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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
|
-
* -
|
|
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
|
|
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({
|
|
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
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
|
|
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
|
|
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({
|
|
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
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
-
|
|
694
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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 `
|
|
732
|
-
const userEntity = await this.
|
|
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
|
}
|