@nauth-toolkit/core 0.1.0 → 0.1.3

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 (184) hide show
  1. package/LICENSE +90 -0
  2. package/README.md +30 -0
  3. package/package.json +7 -2
  4. package/jest.config.js +0 -15
  5. package/jest.setup.ts +0 -6
  6. package/src/adapters/database-columns.ts +0 -165
  7. package/src/adapters/express.adapter.ts +0 -385
  8. package/src/adapters/fastify.adapter.ts +0 -416
  9. package/src/adapters/index.ts +0 -16
  10. package/src/adapters/storage.factory.ts +0 -143
  11. package/src/bootstrap.ts +0 -374
  12. package/src/dto/auth-challenge.dto.ts +0 -231
  13. package/src/dto/auth-response.dto.ts +0 -253
  14. package/src/dto/challenge-response.dto.ts +0 -234
  15. package/src/dto/change-password-request.dto.ts +0 -50
  16. package/src/dto/change-password-response.dto.ts +0 -29
  17. package/src/dto/change-password.dto.ts +0 -57
  18. package/src/dto/error-response.dto.ts +0 -136
  19. package/src/dto/get-available-methods.dto.ts +0 -55
  20. package/src/dto/get-challenge-data-response.dto.ts +0 -28
  21. package/src/dto/get-challenge-data.dto.ts +0 -69
  22. package/src/dto/get-client-info.dto.ts +0 -104
  23. package/src/dto/get-device-token-response.dto.ts +0 -25
  24. package/src/dto/get-events-by-type.dto.ts +0 -76
  25. package/src/dto/get-ip-address-response.dto.ts +0 -24
  26. package/src/dto/get-mfa-status.dto.ts +0 -94
  27. package/src/dto/get-risk-assessment-history.dto.ts +0 -39
  28. package/src/dto/get-session-id-response.dto.ts +0 -25
  29. package/src/dto/get-setup-data-response.dto.ts +0 -31
  30. package/src/dto/get-setup-data.dto.ts +0 -75
  31. package/src/dto/get-suspicious-activity.dto.ts +0 -42
  32. package/src/dto/get-user-agent-response.dto.ts +0 -23
  33. package/src/dto/get-user-auth-history.dto.ts +0 -95
  34. package/src/dto/get-user-by-email.dto.ts +0 -61
  35. package/src/dto/get-user-by-id.dto.ts +0 -46
  36. package/src/dto/get-user-devices.dto.ts +0 -53
  37. package/src/dto/get-user-response.dto.ts +0 -17
  38. package/src/dto/has-provider.dto.ts +0 -56
  39. package/src/dto/index.ts +0 -57
  40. package/src/dto/is-trusted-device-response.dto.ts +0 -34
  41. package/src/dto/list-providers-response.dto.ts +0 -23
  42. package/src/dto/login.dto.ts +0 -95
  43. package/src/dto/logout-all-response.dto.ts +0 -24
  44. package/src/dto/logout-all.dto.ts +0 -65
  45. package/src/dto/logout-response.dto.ts +0 -25
  46. package/src/dto/logout.dto.ts +0 -64
  47. package/src/dto/refresh-token.dto.ts +0 -36
  48. package/src/dto/remove-devices.dto.ts +0 -85
  49. package/src/dto/resend-code-response.dto.ts +0 -32
  50. package/src/dto/resend-code.dto.ts +0 -51
  51. package/src/dto/reset-password.dto.ts +0 -115
  52. package/src/dto/respond-challenge.dto.ts +0 -272
  53. package/src/dto/set-mfa-exemption.dto.ts +0 -112
  54. package/src/dto/set-must-change-password-response.dto.ts +0 -27
  55. package/src/dto/set-must-change-password.dto.ts +0 -46
  56. package/src/dto/set-preferred-method.dto.ts +0 -80
  57. package/src/dto/setup-mfa.dto.ts +0 -98
  58. package/src/dto/signup.dto.ts +0 -174
  59. package/src/dto/social-auth.dto.ts +0 -422
  60. package/src/dto/trust-device-response.dto.ts +0 -30
  61. package/src/dto/trust-device.dto.ts +0 -9
  62. package/src/dto/update-user-attributes-request.dto.ts +0 -51
  63. package/src/dto/user-response.dto.ts +0 -138
  64. package/src/dto/user-update.dto.ts +0 -222
  65. package/src/dto/verify-email.dto.ts +0 -313
  66. package/src/dto/verify-mfa-code.dto.ts +0 -103
  67. package/src/dto/verify-phone-by-sub.dto.ts +0 -78
  68. package/src/dto/verify-phone.dto.ts +0 -245
  69. package/src/entities/auth-audit.entity.ts +0 -232
  70. package/src/entities/challenge-session.entity.ts +0 -116
  71. package/src/entities/index.ts +0 -29
  72. package/src/entities/login-attempt.entity.ts +0 -64
  73. package/src/entities/mfa-device.entity.ts +0 -151
  74. package/src/entities/rate-limit.entity.ts +0 -44
  75. package/src/entities/session.entity.ts +0 -180
  76. package/src/entities/social-account.entity.ts +0 -96
  77. package/src/entities/storage-lock.entity.ts +0 -39
  78. package/src/entities/trusted-device.entity.ts +0 -112
  79. package/src/entities/user.entity.ts +0 -243
  80. package/src/entities/verification-token.entity.ts +0 -141
  81. package/src/enums/auth-audit-event-type.enum.ts +0 -360
  82. package/src/enums/error-codes.enum.ts +0 -420
  83. package/src/enums/mfa-method.enum.ts +0 -97
  84. package/src/enums/risk-factor.enum.ts +0 -111
  85. package/src/exceptions/nauth.exception.ts +0 -231
  86. package/src/handlers/auth.handler.ts +0 -260
  87. package/src/handlers/client-info.handler.ts +0 -101
  88. package/src/handlers/csrf.handler.ts +0 -156
  89. package/src/handlers/token-delivery.handler.ts +0 -118
  90. package/src/index.ts +0 -118
  91. package/src/interfaces/client-info.interface.ts +0 -85
  92. package/src/interfaces/config.interface.ts +0 -2135
  93. package/src/interfaces/entities.interface.ts +0 -226
  94. package/src/interfaces/index.ts +0 -15
  95. package/src/interfaces/logger.interface.ts +0 -283
  96. package/src/interfaces/mfa-provider.interface.ts +0 -154
  97. package/src/interfaces/oauth.interface.ts +0 -148
  98. package/src/interfaces/provider.interface.ts +0 -47
  99. package/src/interfaces/social-auth-provider.interface.ts +0 -131
  100. package/src/interfaces/storage-adapter.interface.ts +0 -82
  101. package/src/interfaces/template.interface.ts +0 -510
  102. package/src/interfaces/token-verifier.interface.ts +0 -110
  103. package/src/internal.ts +0 -178
  104. package/src/platform/interfaces.ts +0 -299
  105. package/src/schemas/auth-config.schema.ts +0 -646
  106. package/src/services/adaptive-mfa-decision.service.spec.ts +0 -1058
  107. package/src/services/adaptive-mfa-decision.service.ts +0 -457
  108. package/src/services/auth-audit.service.spec.ts +0 -675
  109. package/src/services/auth-audit.service.ts +0 -558
  110. package/src/services/auth-challenge-helper.service.spec.ts +0 -3227
  111. package/src/services/auth-challenge-helper.service.ts +0 -825
  112. package/src/services/auth-flow-context-builder.service.ts +0 -520
  113. package/src/services/auth-flow-rules.ts +0 -202
  114. package/src/services/auth-flow-state-definitions.ts +0 -190
  115. package/src/services/auth-flow-state-machine.service.ts +0 -207
  116. package/src/services/auth-flow-state-machine.types.ts +0 -316
  117. package/src/services/auth.service.spec.ts +0 -4195
  118. package/src/services/auth.service.ts +0 -3727
  119. package/src/services/challenge.service.spec.ts +0 -1363
  120. package/src/services/challenge.service.ts +0 -696
  121. package/src/services/client-info.service.spec.ts +0 -572
  122. package/src/services/client-info.service.ts +0 -374
  123. package/src/services/csrf.service.ts +0 -54
  124. package/src/services/email-verification.service.spec.ts +0 -1229
  125. package/src/services/email-verification.service.ts +0 -578
  126. package/src/services/geo-location.service.spec.ts +0 -603
  127. package/src/services/geo-location.service.ts +0 -599
  128. package/src/services/index.ts +0 -13
  129. package/src/services/jwt.service.spec.ts +0 -882
  130. package/src/services/jwt.service.ts +0 -621
  131. package/src/services/mfa-base.service.spec.ts +0 -246
  132. package/src/services/mfa-base.service.ts +0 -611
  133. package/src/services/mfa.service.spec.ts +0 -693
  134. package/src/services/mfa.service.ts +0 -960
  135. package/src/services/password.service.spec.ts +0 -166
  136. package/src/services/password.service.ts +0 -309
  137. package/src/services/phone-verification.service.spec.ts +0 -1120
  138. package/src/services/phone-verification.service.ts +0 -751
  139. package/src/services/risk-detection.service.spec.ts +0 -1292
  140. package/src/services/risk-detection.service.ts +0 -1012
  141. package/src/services/risk-scoring.service.spec.ts +0 -204
  142. package/src/services/risk-scoring.service.ts +0 -131
  143. package/src/services/session.service.spec.ts +0 -1293
  144. package/src/services/session.service.ts +0 -803
  145. package/src/services/social-account.service.spec.ts +0 -725
  146. package/src/services/social-auth-base.service.spec.ts +0 -418
  147. package/src/services/social-auth-base.service.ts +0 -581
  148. package/src/services/social-auth.service.spec.ts +0 -238
  149. package/src/services/social-auth.service.ts +0 -436
  150. package/src/services/social-provider-registry.service.spec.ts +0 -238
  151. package/src/services/social-provider-registry.service.ts +0 -122
  152. package/src/services/trusted-device.service.spec.ts +0 -505
  153. package/src/services/trusted-device.service.ts +0 -339
  154. package/src/storage/account-lockout-storage.service.spec.ts +0 -310
  155. package/src/storage/account-lockout-storage.service.ts +0 -89
  156. package/src/storage/index.ts +0 -3
  157. package/src/storage/memory-storage.adapter.ts +0 -443
  158. package/src/storage/rate-limit-storage.service.spec.ts +0 -247
  159. package/src/storage/rate-limit-storage.service.ts +0 -38
  160. package/src/templates/html-template.engine.spec.ts +0 -161
  161. package/src/templates/html-template.engine.ts +0 -688
  162. package/src/templates/index.ts +0 -7
  163. package/src/utils/common-passwords.spec.ts +0 -230
  164. package/src/utils/common-passwords.ts +0 -170
  165. package/src/utils/context-storage.ts +0 -188
  166. package/src/utils/cookie-names.util.ts +0 -67
  167. package/src/utils/cookies.util.ts +0 -94
  168. package/src/utils/index.ts +0 -12
  169. package/src/utils/ip-extractor.spec.ts +0 -330
  170. package/src/utils/ip-extractor.ts +0 -220
  171. package/src/utils/nauth-logger.spec.ts +0 -388
  172. package/src/utils/nauth-logger.ts +0 -215
  173. package/src/utils/pii-redactor.spec.ts +0 -130
  174. package/src/utils/pii-redactor.ts +0 -288
  175. package/src/utils/setup/get-repositories.ts +0 -140
  176. package/src/utils/setup/init-services.ts +0 -422
  177. package/src/utils/setup/init-social.ts +0 -189
  178. package/src/utils/setup/init-storage.ts +0 -94
  179. package/src/utils/setup/register-mfa.ts +0 -165
  180. package/src/utils/setup/run-nauth-migrations.ts +0 -61
  181. package/src/utils/token-delivery-policy.ts +0 -38
  182. package/src/validators/template.validator.ts +0 -219
  183. package/tsconfig.json +0 -37
  184. package/tsconfig.lint.json +0 -6
@@ -1,960 +0,0 @@
1
- import { Repository } from 'typeorm';
2
- import { BaseMFADevice, BaseUser } from '../entities';
3
- import { IUser, IMFADevice } from '../interfaces/entities.interface';
4
- import { IMFAProviderService } from '../interfaces/mfa-provider.interface';
5
- import { NAuthException } from '../exceptions/nauth.exception';
6
- import { AuthErrorCode } from '../enums/error-codes.enum';
7
- import { MFAMethod, MFADeviceMethod } from '../enums/mfa-method.enum';
8
- import { ChallengeService } from './challenge.service';
9
- import { AuthChallenge } from '../dto/auth-challenge.dto';
10
- import { NAuthConfig } from '../interfaces/config.interface';
11
- import { NAuthLogger } from '../utils/nauth-logger';
12
- import { InternalAuthAuditService as AuthAuditService } from './auth-audit.service';
13
- import { AuthAuditEventType } from '../enums/auth-audit-event-type.enum';
14
- import { ClientInfoService } from './client-info.service';
15
- import {
16
- GetAvailableMethodsDTO,
17
- GetAvailableMethodsResponseDTO,
18
- GetChallengeDataDTO,
19
- GetChallengeDataResponseDTO,
20
- GetMFAStatusDTO,
21
- GetMFAStatusResponseDTO,
22
- GetSetupDataDTO,
23
- GetSetupDataResponseDTO,
24
- GetUserDevicesDTO,
25
- GetUserDevicesResponseDTO,
26
- HasProviderDTO,
27
- HasProviderResponseDTO,
28
- ListProvidersResponseDTO,
29
- RemoveDevicesDTO,
30
- RemoveDevicesResponseDTO,
31
- SetMFAExemptionDTO,
32
- SetMFAExemptionResponseDTO,
33
- SetPreferredMethodDTO,
34
- SetPreferredMethodResponseDTO,
35
- SetupMFADTO,
36
- SetupMFAResponseDTO,
37
- VerifyMFACodeDTO,
38
- VerifyMFACodeResponseDTO,
39
- } from '../dto';
40
-
41
- /**
42
- * MFA Service Registry
43
- *
44
- * Central registry for managing MFA provider services.
45
- * Routes requests to the appropriate provider based on method name.
46
- *
47
- * Provider services (TOTP, SMS, Passkey) automatically register themselves
48
- * when their modules are imported via OnModuleInit.
49
- *
50
- * **Key Features:**
51
- * - Provider registration and lookup
52
- * - Unified interface for MFA operations
53
- * - Routing verification requests to correct provider
54
- * - Device management operations
55
- *
56
- * @example
57
- * ```typescript
58
- * @Controller('auth')
59
- * export class AuthController {
60
- * constructor(private readonly mfaService: MFAService) {}
61
- *
62
- * @Post('mfa/verify')
63
- * async verifyMFA(@Body() dto: { method: string; code: string }) {
64
- * const provider = this.mfaService.getProvider(dto.method);
65
- * return await provider.verify(user, dto.code);
66
- * }
67
- * }
68
- * ```
69
- */
70
- export class MFAService {
71
- private readonly providers = new Map<string, IMFAProviderService>();
72
-
73
- constructor(
74
- private readonly mfaDeviceRepository: Repository<BaseMFADevice>,
75
- private readonly userRepository: Repository<BaseUser>,
76
- private readonly challengeService?: ChallengeService,
77
- private readonly config?: NAuthConfig,
78
- private readonly logger?: NAuthLogger,
79
- private readonly auditService?: AuthAuditService,
80
- private readonly clientInfoService?: ClientInfoService,
81
- ) {}
82
-
83
- /**
84
- * Register an MFA provider
85
- *
86
- * Called automatically by provider modules during initialization.
87
- * Provider method names must be unique.
88
- *
89
- * @param provider - Provider service instance (must have methodName property)
90
- * @throws {NAuthException} If provider is already registered
91
- *
92
- * @example
93
- * ```typescript
94
- * // In provider module's OnModuleInit
95
- * this.mfaService.registerProvider(this.totpProvider);
96
- * ```
97
- */
98
- registerProvider(provider: IMFAProviderService): void {
99
- const name = provider.methodName;
100
- if (this.providers.has(name)) {
101
- throw new NAuthException(AuthErrorCode.VALIDATION_FAILED, `MFA provider '${name}' is already registered`);
102
- }
103
- this.providers.set(name, provider);
104
- }
105
-
106
- /**
107
- * Get a provider by method name
108
- *
109
- * @param methodName - Method name (e.g., 'totp', 'sms', 'passkey')
110
- * @returns Provider service instance
111
- * @throws {NAuthException} If provider is not registered
112
- *
113
- * @example
114
- * ```typescript
115
- * const totpProvider = this.mfaService.getProvider('totp');
116
- * const setupData = await totpProvider.setup(user);
117
- * ```
118
- */
119
- getProvider(methodName: string): IMFAProviderService {
120
- const provider = this.providers.get(methodName);
121
- if (!provider) {
122
- throw new NAuthException(
123
- AuthErrorCode.VALIDATION_FAILED,
124
- `MFA provider '${methodName}' is not registered. Import the provider module (e.g., TOTPMFAModule) and ensure it's properly configured.`,
125
- );
126
- }
127
- return provider;
128
- }
129
-
130
- /**
131
- * Check if a provider is registered
132
- *
133
- * @param dto - Request DTO with method name
134
- * @returns Response DTO with hasProvider flag
135
- *
136
- * @example
137
- * ```typescript
138
- * const result = await this.mfaService.hasProvider({ methodName: 'totp' });
139
- * if (result.hasProvider) {
140
- * // TOTP is available
141
- * }
142
- * ```
143
- */
144
- hasProvider(dto: HasProviderDTO): HasProviderResponseDTO {
145
- return {
146
- hasProvider: this.providers.has(dto.methodName),
147
- };
148
- }
149
-
150
- /**
151
- * Get all registered provider method names
152
- *
153
- * @returns Response DTO with array of method names
154
- *
155
- * @example
156
- * ```typescript
157
- * const result = this.mfaService.listProviders(); // { providers: ['totp', 'sms', 'passkey'] }
158
- * ```
159
- */
160
- listProviders(): ListProvidersResponseDTO {
161
- return {
162
- providers: Array.from(this.providers.keys()),
163
- };
164
- }
165
-
166
- /**
167
- * Get available MFA methods for a user
168
- *
169
- * Returns list of methods that are:
170
- * - Registered as providers
171
- * - Allowed by configuration
172
- *
173
- * This returns ALL methods that can be set up, not just ones the user has configured.
174
- * Use getUserDevices() to check which methods the user has actually set up.
175
- *
176
- * @param dto - Request DTO with user sub
177
- * @returns Response DTO with array of available method names
178
- *
179
- * @example
180
- * ```typescript
181
- * const result = await this.mfaService.getAvailableMethods({ sub: user.sub });
182
- * // Returns: { availableMethods: ['totp', 'sms', 'passkey'] }
183
- * ```
184
- */
185
- async getAvailableMethods(dto: GetAvailableMethodsDTO): Promise<GetAvailableMethodsResponseDTO> {
186
- // Look up user by sub to validate user exists
187
- const userEntity = await this.userRepository.findOne({ where: { sub: dto.sub } });
188
- if (!userEntity) {
189
- throw new NAuthException(AuthErrorCode.NOT_FOUND, 'User not found');
190
- }
191
-
192
- const available: string[] = [];
193
-
194
- for (const [methodName, provider] of this.providers.entries()) {
195
- // Check if method is allowed by configuration
196
- if (!provider.isMethodAllowed()) {
197
- continue;
198
- }
199
-
200
- // Return all allowed methods (whether user has set them up or not)
201
- available.push(methodName);
202
- }
203
-
204
- return {
205
- availableMethods: available,
206
- };
207
- }
208
-
209
- /**
210
- * Verify MFA code using appropriate provider
211
- *
212
- * Routes the verification request to the correct provider based on method name.
213
- *
214
- * @param dto - Request DTO with user sub, method name, code, and optional device ID
215
- * @returns Response DTO with verification result
216
- * @throws {NAuthException} If method is not available or verification fails
217
- *
218
- * @example
219
- * ```typescript
220
- * // Verify TOTP code
221
- * const result = await this.mfaService.verifyCode({
222
- * sub: user.sub,
223
- * methodName: 'totp',
224
- * code: '123456'
225
- * });
226
- *
227
- * // Verify backup code
228
- * const result = await this.mfaService.verifyCode({
229
- * sub: user.sub,
230
- * methodName: 'backup',
231
- * code: 'ABC12345'
232
- * });
233
- * ```
234
- */
235
- async verifyCode(dto: VerifyMFACodeDTO): Promise<VerifyMFACodeResponseDTO> {
236
- // Look up user by sub
237
- const userEntity = await this.userRepository.findOne({ where: { sub: dto.sub } });
238
- if (!userEntity) {
239
- throw new NAuthException(AuthErrorCode.NOT_FOUND, 'User not found');
240
- }
241
- const user = userEntity as unknown as IUser;
242
-
243
- // Handle backup codes specially (not a provider, uses base class helper)
244
- if (dto.methodName === MFAMethod.BACKUP) {
245
- // Get any provider to access backup code verification
246
- // All providers extend BaseMFAProviderService which has verifyBackupCode
247
- const firstProvider = Array.from(this.providers.values())[0];
248
- if (firstProvider && 'verifyBackupCode' in firstProvider) {
249
- const providerWithBackup = firstProvider as IMFAProviderService & {
250
- verifyBackupCode: (user: IUser, code: string) => Promise<boolean>;
251
- };
252
- const isValid = await providerWithBackup.verifyBackupCode(user, dto.code as string);
253
- return { valid: isValid };
254
- }
255
- throw new NAuthException(AuthErrorCode.VALIDATION_FAILED, 'Backup code verification not available');
256
- }
257
-
258
- // Get provider and verify
259
- const provider = this.getProvider(dto.methodName);
260
- const isValid = await provider.verify(user, dto.code, dto.deviceId);
261
- return { valid: isValid };
262
- }
263
-
264
- /**
265
- * Setup MFA device using appropriate provider
266
- *
267
- * @param dto - Request DTO with user sub, method name, and optional setup data
268
- * @returns Response DTO with provider-specific setup data
269
- *
270
- * @example
271
- * ```typescript
272
- * const result = await this.mfaService.setup({
273
- * sub: user.sub,
274
- * methodName: 'totp'
275
- * });
276
- * // Returns: { setupData: { secret, qrCode, manualEntryKey } }
277
- * ```
278
- */
279
- async setup(dto: SetupMFADTO): Promise<SetupMFAResponseDTO> {
280
- // Look up user by sub
281
- const userEntity = await this.userRepository.findOne({ where: { sub: dto.sub } });
282
- if (!userEntity) {
283
- throw new NAuthException(AuthErrorCode.NOT_FOUND, 'User not found');
284
- }
285
- const user = userEntity as unknown as IUser;
286
-
287
- const provider = this.getProvider(dto.methodName);
288
- const setupData = await provider.setup(user, dto.setupData);
289
- return {
290
- setupData: setupData as Record<string, unknown>,
291
- };
292
- }
293
-
294
- /**
295
- * Get user's MFA devices
296
- *
297
- * @param dto - Request DTO with user sub
298
- * @returns Response DTO with array of MFA devices
299
- *
300
- * @example
301
- * ```typescript
302
- * const result = await this.mfaService.getUserDevices({ sub: user.sub });
303
- * // Returns: { devices: [...] }
304
- * ```
305
- */
306
- async getUserDevices(dto: GetUserDevicesDTO): Promise<GetUserDevicesResponseDTO> {
307
- // Look up user by sub to get internal ID
308
- const userEntity = await this.userRepository.findOne({ where: { sub: dto.sub } });
309
- if (!userEntity) {
310
- throw new NAuthException(AuthErrorCode.NOT_FOUND, 'User not found');
311
- }
312
-
313
- // Only fetch active devices (inactive devices are soft-deleted)
314
- const devices = await this.mfaDeviceRepository.find({
315
- where: { userId: userEntity.id, isActive: true },
316
- order: { createdAt: 'DESC' },
317
- } as Record<string, unknown>);
318
-
319
- return {
320
- devices: devices as unknown as IMFADevice[],
321
- };
322
- }
323
-
324
- /**
325
- * Get comprehensive MFA status for a user
326
- *
327
- * Returns complete MFA configuration status including:
328
- * - Whether MFA is enabled/required
329
- * - Configured and available methods
330
- * - Preferred method
331
- * - Backup codes status
332
- * - MFA exemption information
333
- *
334
- * This method encapsulates all business logic for MFA status,
335
- * ensuring consumer apps don't need to query databases or build responses manually.
336
- *
337
- * @param dto - Request DTO with user sub
338
- * @returns Response DTO with complete MFA status
339
- *
340
- * @example
341
- * ```typescript
342
- * @Get('mfa/status')
343
- * async getMFAStatus(@CurrentUser() user: IUser) {
344
- * return await this.mfaService.getMFAStatus({ sub: user.sub });
345
- * }
346
- * ```
347
- */
348
- async getMFAStatus(dto: GetMFAStatusDTO): Promise<GetMFAStatusResponseDTO> {
349
- // Get user entity with MFA-related fields
350
- // Note: mfaExemptGrantedBy is intentionally excluded as it's sensitive admin information
351
- const userEntity = await this.userRepository.findOne({
352
- select: [
353
- 'id',
354
- 'mfaEnabled',
355
- 'backupCodes',
356
- 'preferredMfaMethod',
357
- 'mfaExempt',
358
- 'mfaExemptReason',
359
- 'mfaExemptGrantedAt',
360
- ],
361
- where: { sub: dto.sub },
362
- });
363
-
364
- if (!userEntity) {
365
- throw new NAuthException(AuthErrorCode.NOT_FOUND, 'User not found');
366
- }
367
-
368
- const enabled = userEntity.mfaEnabled || false;
369
-
370
- // Get available methods (all registered & allowed methods)
371
- const availableMethodsResult = await this.getAvailableMethods({ sub: dto.sub });
372
-
373
- // Add 'backup' to available methods if backup codes are enabled in config
374
- const finalAvailableMethods = [...availableMethodsResult.availableMethods];
375
- if (this.config?.mfa?.backup?.enabled) {
376
- if (!finalAvailableMethods.includes(MFAMethod.BACKUP)) {
377
- finalAvailableMethods.push(MFAMethod.BACKUP);
378
- }
379
- }
380
-
381
- // Get user's configured devices
382
- const devicesResult = await this.getUserDevices({ sub: dto.sub });
383
- const configuredMethods = [
384
- ...new Set(devicesResult.devices.filter((d) => d.isActive).map((d) => d.type)),
385
- ] as MFADeviceMethod[];
386
-
387
- // Determine if MFA is required based on config and user state
388
- const required = enabled && configuredMethods.length > 0;
389
-
390
- // Check backup codes
391
- const hasBackupCodes = !!userEntity.backupCodes && userEntity.backupCodes.length > 0;
392
-
393
- return {
394
- enabled,
395
- required,
396
- configuredMethods,
397
- availableMethods: finalAvailableMethods,
398
- hasBackupCodes,
399
- preferredMethod: userEntity.preferredMfaMethod as MFADeviceMethod | undefined,
400
- mfaExempt: userEntity.mfaExempt || false,
401
- mfaExemptReason: userEntity.mfaExemptReason || null,
402
- mfaExemptGrantedAt: userEntity.mfaExemptGrantedAt || null,
403
- };
404
- }
405
-
406
- /**
407
- * Remove MFA devices by method type
408
- *
409
- * Comprehensive method that handles all aspects of MFA device removal:
410
- * - Looks up user by sub (consumer apps should pass user.sub from @CurrentUser())
411
- * - Validates method type
412
- * - Removes all active devices of the specified method type
413
- * - Updates user's preferred method if the removed method was preferred
414
- * - Updates device primary flags
415
- * - Disables MFA if this was the last device
416
- * - Creates MFA_SETUP_REQUIRED challenge if MFA enforcement requires it
417
- *
418
- * This method encapsulates all database operations related to MFA device removal,
419
- * ensuring the consumer app doesn't need to directly manipulate nauth_* tables.
420
- *
421
- * @param dto - Request DTO with user sub and method type
422
- * @returns Response DTO with deletedCount and whether MFA was disabled
423
- * @throws {NAuthException} If user not found, invalid method type, or no devices found
424
- *
425
- * @example
426
- * ```typescript
427
- * // Consumer app controller
428
- * @Delete('mfa/devices/:method')
429
- * async removeMFAMethod(@CurrentUser() user: IUser, @Param('method') method: string) {
430
- * const result = await this.mfaService.removeDevices({ userSub: user.sub, methodType: method });
431
- * return { message: 'MFA method removed successfully', ...result };
432
- * }
433
- * ```
434
- */
435
- async removeDevices(dto: RemoveDevicesDTO): Promise<RemoveDevicesResponseDTO> {
436
- // Validate method type
437
- const validMethods = [MFAMethod.TOTP, MFAMethod.SMS, MFAMethod.EMAIL, MFAMethod.PASSKEY];
438
- const normalizedMethod = dto.methodType.toLowerCase();
439
- if (!validMethods.includes(normalizedMethod as MFAMethod)) {
440
- throw new NAuthException(
441
- AuthErrorCode.VALIDATION_FAILED,
442
- `Invalid MFA method: ${dto.methodType}. Valid methods are: ${validMethods.join(', ')}`,
443
- );
444
- }
445
-
446
- // Look up user by sub using repository directly (no AuthService dependency needed)
447
- const userEntity = await this.userRepository.findOne({ where: { sub: dto.userSub } });
448
- if (!userEntity) {
449
- throw new NAuthException(AuthErrorCode.NOT_FOUND, 'User entity not found');
450
- }
451
-
452
- const userId = userEntity.id;
453
- if (!userId) {
454
- throw new NAuthException(AuthErrorCode.INTERNAL_ERROR, 'User entity missing internal ID');
455
- }
456
-
457
- // Cast to IUser for type safety
458
- const user = userEntity as unknown as IUser;
459
-
460
- const preferredMethod = userEntity.preferredMfaMethod;
461
- const isPreferredMethod = preferredMethod === normalizedMethod;
462
-
463
- // Get all active devices for this user
464
- const devicesResult = await this.getUserDevices({ sub: dto.userSub });
465
- const activeDevices = devicesResult.devices.filter((d) => d.isActive);
466
-
467
- // Get devices of the method type to remove
468
- const devicesToRemove = activeDevices.filter((d) => d.type.toLowerCase() === normalizedMethod);
469
-
470
- if (devicesToRemove.length === 0) {
471
- throw new NAuthException(
472
- AuthErrorCode.VALIDATION_FAILED,
473
- `No active ${normalizedMethod} MFA devices found for this user`,
474
- );
475
- }
476
-
477
- // Delete all devices of this method type
478
- let deletedCount = 0;
479
- for (const device of devicesToRemove) {
480
- const result = await this.mfaDeviceRepository.delete(device.id);
481
- deletedCount += result.affected || 0;
482
- }
483
-
484
- // Check if any devices remain after removal
485
- const remainingDevicesResult = await this.getUserDevices({ sub: dto.userSub });
486
- const remainingActiveDevices = remainingDevicesResult.devices.filter((d) => d.isActive);
487
- let mfaDisabled = false;
488
-
489
- // If no active devices remain, disable MFA for user
490
- if (remainingActiveDevices.length === 0) {
491
- userEntity.mfaEnabled = false;
492
- userEntity.mfaMethods = [];
493
- userEntity.preferredMfaMethod = null;
494
- await this.userRepository.save(userEntity);
495
- mfaDisabled = true;
496
-
497
- // ============================================================================
498
- // Audit: Record MFA disabled (all devices removed)
499
- // ============================================================================
500
- if (this.auditService && this.clientInfoService) {
501
- try {
502
- await this.auditService?.recordEvent({
503
- userId: user.id,
504
- eventType: AuthAuditEventType.MFA_DISABLED,
505
- eventStatus: 'INFO',
506
- reason: 'all_devices_removed',
507
- description: 'MFA disabled - all devices removed',
508
- // Client info automatically included from context
509
- metadata: {
510
- removedMethod: normalizedMethod,
511
- deletedCount,
512
- },
513
- });
514
- } catch (auditError) {
515
- // Non-blocking: Log but continue
516
- const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
517
- this.logger?.error?.(`Failed to record MFA_DISABLED audit event: ${errorMessage}`, {
518
- error: auditError,
519
- userId: user.id,
520
- });
521
- }
522
- }
523
-
524
- // Automatically create MFA_SETUP_REQUIRED challenge if MFA enforcement requires it
525
- if (this.challengeService && this.config?.mfa?.enabled) {
526
- const enforcement = this.config.mfa.enforcement || 'OPTIONAL';
527
- if (enforcement === 'REQUIRED' || enforcement === 'ADAPTIVE') {
528
- const user = userEntity as unknown as IUser;
529
- try {
530
- // Client info (ipAddress, userAgent) automatically extracted from ClientInfoService
531
- await this.challengeService.createChallengeSession(user, AuthChallenge.MFA_SETUP_REQUIRED, {
532
- allowedMethods: this.config.mfa.allowedMethods || [],
533
- requiresSetup: true,
534
- });
535
- this.logger?.log?.(`Created MFA_SETUP_REQUIRED challenge for user ${user.sub} after MFA removal`);
536
- } catch (error) {
537
- // Log but don't fail the removal if challenge creation fails
538
- this.logger?.warn?.(`Failed to create MFA_SETUP_REQUIRED challenge after MFA removal: ${error}`);
539
- }
540
- }
541
- }
542
- } else {
543
- // Update mfaMethods array with remaining methods
544
- const remainingMethods = [...new Set(remainingActiveDevices.map((d) => d.type))];
545
- userEntity.mfaMethods = remainingMethods;
546
-
547
- // If the removed method was preferred, update preferred method and device primary flags
548
- if (isPreferredMethod) {
549
- const newPreferredMethod = remainingActiveDevices[0].type;
550
- userEntity.preferredMfaMethod = newPreferredMethod;
551
- await this.userRepository.save(userEntity);
552
-
553
- // Update device primary flags - set first remaining device as primary
554
- if (remainingActiveDevices[0].id) {
555
- await this.mfaDeviceRepository.update({ id: remainingActiveDevices[0].id }, { isPrimary: true });
556
- }
557
-
558
- // Unset primary flag on other devices
559
- for (let i = 1; i < remainingActiveDevices.length; i++) {
560
- if (remainingActiveDevices[i].id) {
561
- await this.mfaDeviceRepository.update({ id: remainingActiveDevices[i].id }, { isPrimary: false });
562
- }
563
- }
564
-
565
- this.logger?.log?.(`Updated preferred MFA method to ${newPreferredMethod} after removing ${normalizedMethod}`);
566
- } else {
567
- // No preferred method change needed, just update mfaMethods
568
- await this.userRepository.save(userEntity);
569
- }
570
- }
571
-
572
- // ============================================================================
573
- // Audit: Record MFA device removal
574
- // ============================================================================
575
- if (deletedCount > 0 && this.auditService && this.clientInfoService) {
576
- try {
577
- const user = userEntity as unknown as IUser;
578
- await this.auditService?.recordEvent({
579
- userId: user.id,
580
- eventType: AuthAuditEventType.MFA_DEVICE_REMOVED,
581
- eventStatus: 'INFO',
582
- metadata: {
583
- method: normalizedMethod,
584
- deletedCount,
585
- remainingDevices: remainingActiveDevices.length,
586
- mfaDisabled,
587
- },
588
- // Client info automatically included from context
589
- });
590
- } catch (auditError) {
591
- // Non-blocking: Log but continue
592
- const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
593
- this.logger?.error?.(`Failed to record MFA_DEVICE_REMOVED audit event: ${errorMessage}`, {
594
- error: auditError,
595
- userId: user.id,
596
- method: normalizedMethod,
597
- });
598
- }
599
- }
600
-
601
- return { deletedCount, mfaDisabled };
602
- }
603
-
604
- /**
605
- * Set preferred MFA method for a user
606
- *
607
- * Updates the user's preferred MFA method and device primary flags.
608
- * Validates that the method is configured for the user before setting it as preferred.
609
- *
610
- * This method encapsulates all database operations related to preferred method updates,
611
- * ensuring the consumer app doesn't need to directly manipulate nauth_* tables.
612
- *
613
- * @param dto - Request DTO with user sub and method type
614
- * @returns Response DTO with success message
615
- * @throws {NAuthException} If user not found, invalid method type, or method not configured
616
- *
617
- * @example
618
- * ```typescript
619
- * // Consumer app controller
620
- * @Put('mfa/preferred')
621
- * async setPreferredMFAMethod(@CurrentUser() user: IUser, @Body() body: { method: string }) {
622
- * return await this.mfaService.setPreferredMethod({ userSub: user.sub, methodType: body.method });
623
- * }
624
- * ```
625
- */
626
- async setPreferredMethod(dto: SetPreferredMethodDTO): Promise<SetPreferredMethodResponseDTO> {
627
- // Validate method type
628
- const validMethods = [MFAMethod.TOTP, MFAMethod.SMS, MFAMethod.EMAIL, MFAMethod.PASSKEY];
629
- const normalizedMethod = dto.methodType.toLowerCase();
630
- if (!validMethods.includes(normalizedMethod as MFAMethod)) {
631
- throw new NAuthException(
632
- AuthErrorCode.VALIDATION_FAILED,
633
- `Invalid MFA method: ${dto.methodType}. Valid methods are: ${validMethods.join(', ')}`,
634
- );
635
- }
636
-
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 NAuthException(AuthErrorCode.NOT_FOUND, 'User not found');
641
- }
642
-
643
- const userId = userEntity.id;
644
- if (!userId) {
645
- throw new NAuthException(AuthErrorCode.INTERNAL_ERROR, 'User entity missing internal ID');
646
- }
647
-
648
- // Cast to IUser for type safety
649
- const user = userEntity as unknown as IUser;
650
-
651
- // Verify user has this method configured
652
- const devicesResult = await this.getUserDevices({ sub: dto.userSub });
653
- // Normalize device types for comparison (database might store in different case)
654
- const preferredDevice = devicesResult.devices.find((d) => d.type.toLowerCase() === normalizedMethod && d.isActive);
655
-
656
- if (!preferredDevice) {
657
- throw new NAuthException(
658
- AuthErrorCode.VALIDATION_FAILED,
659
- `MFA method '${normalizedMethod}' is not configured for this user`,
660
- );
661
- }
662
-
663
- // Update user's preferred method directly via repository
664
- await this.userRepository.update(
665
- { id: userId },
666
- {
667
- preferredMfaMethod: normalizedMethod as MFADeviceMethod,
668
- },
669
- );
670
-
671
- // Update device isPrimary flags: set preferred device as primary, unset others
672
- const activeDevices = devicesResult.devices.filter((d) => d.isActive);
673
- for (const device of activeDevices) {
674
- await this.mfaDeviceRepository.update({ id: device.id }, { isPrimary: device.id === preferredDevice.id });
675
- }
676
-
677
- this.logger?.log?.(`Device ${preferredDevice.id} set as primary for user ${dto.userSub}`);
678
-
679
- // ============================================================================
680
- // Audit: Record preferred MFA method update
681
- // ============================================================================
682
- if (this.auditService && this.clientInfoService) {
683
- try {
684
- const previousMethod = userEntity.preferredMfaMethod;
685
- await this.auditService?.recordEvent({
686
- userId: user.id,
687
- eventType: AuthAuditEventType.MFA_PREFERRED_METHOD_UPDATED,
688
- eventStatus: 'INFO',
689
- metadata: {
690
- // Client info automatically included from context
691
- previousMethod: previousMethod || null,
692
- newMethod: normalizedMethod,
693
- deviceId: preferredDevice.id,
694
- },
695
- });
696
- } catch (auditError) {
697
- // Non-blocking: Log but continue
698
- const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
699
- this.logger?.error?.(`Failed to record MFA_PREFERRED_METHOD_UPDATED audit event: ${errorMessage}`, {
700
- error: auditError,
701
- userId: user.id,
702
- method: normalizedMethod,
703
- });
704
- }
705
- }
706
-
707
- return {
708
- message: 'Preferred method updated',
709
- };
710
- }
711
-
712
- /**
713
- * Grant or revoke a user's exemption from multi-factor authentication (MFA) requirements.
714
- *
715
- * SECURITY: This admin-only operation updates the user's MFA exemption status, logs the action,
716
- * and records an audit event. MFA exemption bypasses MFA at login, but all other security controls remain enforced.
717
- *
718
- * @param dto - Request DTO with user sub, exempt flag, reason, and grantedBy
719
- * @returns Response DTO with updated exemption fields
720
- * @throws {NAuthException} If the user is not found
721
- *
722
- * @example
723
- * ```typescript
724
- * // Grant MFA exemption
725
- * await mfaService.setMFAExemption({
726
- * userSub: 'user-uuid',
727
- * exempt: true,
728
- * reason: 'Business partner requires MFA bypass',
729
- * grantedBy: 'admin@example.com'
730
- * });
731
- *
732
- * // Revoke MFA exemption
733
- * await mfaService.setMFAExemption({
734
- * userSub: 'user-uuid',
735
- * exempt: false,
736
- * reason: 'MFA now mandatory for this user',
737
- * grantedBy: 'admin@example.com'
738
- * });
739
- * ```
740
- */
741
- async setMFAExemption(dto: SetMFAExemptionDTO): Promise<SetMFAExemptionResponseDTO> {
742
- // Find user by sub (external identifier)
743
- const userEntity = await this.userRepository.findOne({ where: { sub: dto.userSub } });
744
- if (!userEntity) {
745
- throw new NAuthException(AuthErrorCode.NOT_FOUND, 'User not found');
746
- }
747
-
748
- const user = userEntity as unknown as IUser;
749
-
750
- // Prepare update
751
- const updateFields: Record<string, unknown> = {
752
- mfaExempt: dto.exempt,
753
- mfaExemptReason: dto.reason || null,
754
- mfaExemptGrantedAt: dto.exempt ? new Date() : null,
755
- mfaExemptGrantedBy: dto.exempt ? dto.grantedBy || null : null,
756
- };
757
-
758
- // If revoking exemption and MFA is required, check if user needs to set up MFA
759
- // Note: This is just for logging - actual MFA setup requirement is checked by state machine on next login
760
- if (!dto.exempt && userEntity.mfaExempt === true && !userEntity.mfaEnabled) {
761
- this.logger?.warn?.(`MFA exemption revoked for user ${dto.userSub} - MFA setup will be required on next login`);
762
- }
763
-
764
- // Update user in database
765
- await this.userRepository.update(userEntity.id, updateFields);
766
-
767
- // Log the exemption change for audit trail
768
- this.logger?.log?.(`MFA exemption ${dto.exempt ? 'granted' : 'revoked'} for user ${dto.userSub}`, {
769
- userSub: dto.userSub,
770
- exempt: dto.exempt,
771
- reason: dto.reason || 'No reason provided',
772
- grantedBy: dto.grantedBy || 'System',
773
- timestamp: new Date().toISOString(),
774
- });
775
-
776
- // ============================================================================
777
- // Audit: Record MFA exemption grant/revoke
778
- // ============================================================================
779
- if (this.auditService && this.clientInfoService) {
780
- try {
781
- await this.auditService.recordEvent({
782
- userId: user.id,
783
- eventType: dto.exempt ? AuthAuditEventType.MFA_EXEMPTION_GRANTED : AuthAuditEventType.MFA_EXEMPTION_REVOKED,
784
- eventStatus: 'INFO',
785
- performedBy: dto.grantedBy || null,
786
- // Client info automatically included from context
787
- reason: dto.reason || null,
788
- metadata: {
789
- previousExemptStatus: userEntity.mfaExempt,
790
- newExemptStatus: dto.exempt,
791
- },
792
- });
793
- } catch (auditError) {
794
- // Non-blocking: Log but continue
795
- const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
796
- this.logger?.error?.(`Failed to record MFA exemption audit event: ${errorMessage}`, {
797
- error: auditError,
798
- userId: user.id,
799
- });
800
- }
801
- }
802
-
803
- // Fetch updated user to return exemption fields
804
- const exemptionData = await this.userRepository.findOne({
805
- where: { id: userEntity.id },
806
- select: ['mfaExempt', 'mfaExemptReason', 'mfaExemptGrantedAt'],
807
- });
808
-
809
- if (!exemptionData) {
810
- throw new NAuthException(AuthErrorCode.NOT_FOUND, 'User not found after update');
811
- }
812
-
813
- return {
814
- mfaExempt: exemptionData.mfaExempt || false,
815
- mfaExemptReason: exemptionData.mfaExemptReason || null,
816
- mfaExemptGrantedAt: exemptionData.mfaExemptGrantedAt || null,
817
- };
818
- }
819
-
820
- /**
821
- * Get MFA setup data during MFA_SETUP_REQUIRED challenge
822
- *
823
- * Returns provider-specific setup data:
824
- * - TOTP: { secret, qrCode, manualEntryKey }
825
- * - SMS: { maskedPhone } or error if phone required
826
- * - Passkey: WebAuthn registration options
827
- *
828
- * @param dto - Request DTO with session token, method, and optional setup data
829
- * @returns Response DTO with provider-specific setup data
830
- * @throws {NAuthException} INVALID_CHALLENGE_SESSION | VALIDATION_FAILED | PHONE_REQUIRED
831
- *
832
- * @example
833
- * ```typescript
834
- * const result = await mfaService.getSetupData({
835
- * session: 'session-token',
836
- * method: 'totp'
837
- * });
838
- * // Returns: { setupData: { secret: '...', qrCode: '...', manualEntryKey: '...' } }
839
- *
840
- * const result = await mfaService.getSetupData({
841
- * session: 'session-token',
842
- * method: 'sms',
843
- * setupData: { phoneNumber: '+1234567890' }
844
- * });
845
- * // Returns: { setupData: { maskedPhone: '***-***-7890' } }
846
- * ```
847
- */
848
- async getSetupData(dto: GetSetupDataDTO): Promise<GetSetupDataResponseDTO> {
849
- if (!this.challengeService) {
850
- throw new NAuthException(AuthErrorCode.INTERNAL_ERROR, 'Challenge service is not available');
851
- }
852
-
853
- this.logger?.debug?.(`Getting MFA setup data: session=${dto.session}, method=${dto.method}`);
854
-
855
- // Validate session and ensure it's MFA_SETUP_REQUIRED
856
- const challengeSession = await this.challengeService.validateSession(dto.session);
857
-
858
- if (challengeSession.challengeName !== AuthChallenge.MFA_SETUP_REQUIRED) {
859
- throw new NAuthException(
860
- AuthErrorCode.VALIDATION_FAILED,
861
- `Cannot get setup data: expected MFA_SETUP_REQUIRED challenge, got ${challengeSession.challengeName}`,
862
- );
863
- }
864
-
865
- // Get user from session
866
- const user = challengeSession.user;
867
- if (!user) {
868
- throw new NAuthException(AuthErrorCode.VALIDATION_FAILED, 'Challenge session has no associated user');
869
- }
870
-
871
- // Get provider and call setup
872
- // Pass challenge session ID in setupData so provider can link verification tokens
873
- const setupDataWithSession = {
874
- ...(dto.setupData || {}),
875
- challengeSessionId: challengeSession.id,
876
- };
877
- this.logger?.debug?.(`Passing challengeSessionId=${challengeSession.id} to ${dto.method} provider for MFA setup`);
878
- const provider = this.getProvider(dto.method);
879
- const result = await provider.setup(user, setupDataWithSession);
880
-
881
- this.logger?.debug?.(`MFA setup data generated: method=${dto.method}, user=${user.sub}`);
882
-
883
- return {
884
- setupData: result as Record<string, unknown>,
885
- };
886
- }
887
-
888
- /**
889
- * Get MFA challenge data during MFA_REQUIRED challenge
890
- *
891
- * Currently only used for passkey authentication to get WebAuthn options.
892
- * SMS/TOTP codes are sent automatically when the challenge is created.
893
- *
894
- * @param dto - Request DTO with session token and method
895
- * @returns Response DTO with provider-specific challenge data
896
- * @throws {NAuthException} INVALID_CHALLENGE_SESSION | VALIDATION_FAILED
897
- *
898
- * @example
899
- * ```typescript
900
- * const result = await mfaService.getChallengeData({
901
- * session: 'session-token',
902
- * method: 'passkey'
903
- * });
904
- * // Returns: { challengeData: { challenge: '...', allowCredentials: [...], ... } }
905
- * ```
906
- */
907
- async getChallengeData(dto: GetChallengeDataDTO): Promise<GetChallengeDataResponseDTO> {
908
- if (!this.challengeService) {
909
- throw new NAuthException(AuthErrorCode.INTERNAL_ERROR, 'Challenge service is not available');
910
- }
911
-
912
- this.logger?.debug?.(`Getting MFA challenge data: session=${dto.session}, method=${dto.method}`);
913
-
914
- // Validate session and ensure it's MFA_REQUIRED
915
- const challengeSession = await this.challengeService.validateSession(dto.session);
916
-
917
- if (challengeSession.challengeName !== AuthChallenge.MFA_REQUIRED) {
918
- throw new NAuthException(
919
- AuthErrorCode.VALIDATION_FAILED,
920
- `Cannot get challenge data: expected MFA_REQUIRED challenge, got ${challengeSession.challengeName}`,
921
- );
922
- }
923
-
924
- // Get user from session
925
- const user = challengeSession.user;
926
- if (!user) {
927
- throw new NAuthException(AuthErrorCode.VALIDATION_FAILED, 'Challenge session has no associated user');
928
- }
929
-
930
- // Get provider and send challenge
931
- const provider = this.getProvider(dto.method);
932
-
933
- if (!provider.sendChallenge) {
934
- throw new NAuthException(
935
- AuthErrorCode.VALIDATION_FAILED,
936
- `MFA method '${dto.method}' does not support challenge data generation`,
937
- );
938
- }
939
-
940
- const challengeData = await provider.sendChallenge(user);
941
-
942
- // For passkey, store the challenge in session metadata for verification
943
- if (dto.method === 'passkey') {
944
- const passkeyOptions = challengeData as { options: { challenge: string } };
945
- const passkeyChallenge = passkeyOptions.options?.challenge;
946
- if (passkeyChallenge) {
947
- await this.challengeService.updateMetadata(dto.session, {
948
- passkeyChallenge,
949
- });
950
- this.logger?.debug?.(`Passkey challenge stored in session metadata: user=${user.sub}`);
951
- }
952
- }
953
-
954
- this.logger?.debug?.(`MFA challenge data generated: method=${dto.method}, user=${user.sub}`);
955
-
956
- return {
957
- challengeData: challengeData as Record<string, unknown>,
958
- };
959
- }
960
- }