@nauth-toolkit/core 0.1.0 → 0.1.5

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 +9 -0
  3. package/package.json +8 -3
  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,693 +0,0 @@
1
- import { Repository } from 'typeorm';
2
- import { MFAService } from './mfa.service';
3
- import { BaseMFADevice, BaseUser } from '../entities';
4
- import { IUser, IMFADevice } from '../interfaces/entities.interface';
5
- import { IMFAProviderService } from '../interfaces/mfa-provider.interface';
6
- import { NAuthException } from '../exceptions/nauth.exception';
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 { AuthAuditService } from './auth-audit.service';
13
- import { AuthAuditEventType } from '../enums/auth-audit-event-type.enum';
14
- import { ClientInfoService } from './client-info.service';
15
-
16
- /**
17
- * MFA Service Unit Tests
18
- *
19
- * Tests MFA provider registry, verification routing, device management,
20
- * and user preference updates.
21
- *
22
- * Platform-agnostic: Uses direct instantiation, no NestJS dependencies.
23
- */
24
- describe('MFAService', () => {
25
- let service: MFAService;
26
- let mockMfaDeviceRepository: jest.Mocked<Repository<BaseMFADevice>>;
27
- let mockUserRepository: jest.Mocked<Repository<BaseUser>>;
28
- let mockChallengeService: jest.Mocked<ChallengeService>;
29
- let mockLogger: jest.Mocked<NAuthLogger>;
30
- let mockAuditService: jest.Mocked<AuthAuditService>;
31
- let mockClientInfoService: jest.Mocked<ClientInfoService>;
32
- let mockProvider1: jest.Mocked<IMFAProviderService>;
33
- let mockProvider2: jest.Mocked<IMFAProviderService>;
34
-
35
- const mockConfig: Partial<NAuthConfig> = {
36
- mfa: {
37
- enabled: true,
38
- enforcement: 'OPTIONAL',
39
- allowedMethods: [MFAMethod.TOTP, MFAMethod.SMS, MFAMethod.PASSKEY],
40
- },
41
- };
42
-
43
- const mockUser: Partial<IUser> = {
44
- id: 1,
45
- sub: 'user-uuid-123',
46
- email: 'test@example.com',
47
- mfaEnabled: true,
48
- mfaMethods: ['totp'],
49
- preferredMfaMethod: 'totp',
50
- };
51
-
52
- const mockDevice: Partial<IMFADevice> = {
53
- id: 1,
54
- userId: 1,
55
- type: 'totp' as MFADeviceMethod,
56
- name: 'My Device',
57
- isActive: true,
58
- isPrimary: true,
59
- createdAt: new Date(),
60
- };
61
-
62
- beforeEach(() => {
63
- // Create mock providers
64
- mockProvider1 = {
65
- methodName: 'totp',
66
- isMethodAllowed: jest.fn().mockReturnValue(true),
67
- setup: jest.fn(),
68
- verifySetup: jest.fn(),
69
- verify: jest.fn(),
70
- } as any;
71
-
72
- mockProvider2 = {
73
- methodName: 'sms',
74
- isMethodAllowed: jest.fn().mockReturnValue(true),
75
- setup: jest.fn(),
76
- verifySetup: jest.fn(),
77
- verify: jest.fn(),
78
- } as any;
79
-
80
- // Create mock repositories
81
- mockMfaDeviceRepository = {
82
- find: jest.fn(),
83
- delete: jest.fn(),
84
- update: jest.fn(),
85
- create: jest.fn(),
86
- save: jest.fn(),
87
- } as any;
88
-
89
- mockUserRepository = {
90
- findOne: jest.fn(),
91
- save: jest.fn(),
92
- update: jest.fn(),
93
- } as any;
94
-
95
- // Create mock services
96
- mockChallengeService = {
97
- createChallengeSession: jest.fn(),
98
- } as any;
99
-
100
- mockLogger = {
101
- log: jest.fn(),
102
- error: jest.fn(),
103
- warn: jest.fn(),
104
- debug: jest.fn(),
105
- } as any;
106
-
107
- mockAuditService = {
108
- recordEvent: jest.fn(),
109
- } as any;
110
-
111
- mockClientInfoService = {
112
- get: jest.fn(),
113
- } as any;
114
-
115
- // Instantiate service directly
116
- service = new MFAService(
117
- mockMfaDeviceRepository,
118
- mockUserRepository,
119
- mockChallengeService,
120
- mockConfig as NAuthConfig,
121
- mockLogger,
122
- mockAuditService,
123
- mockClientInfoService,
124
- );
125
- });
126
-
127
- afterEach(() => {
128
- jest.clearAllMocks();
129
- });
130
-
131
- // ============================================================================
132
- // Service Initialization
133
- // ============================================================================
134
-
135
- it('should be defined', () => {
136
- expect(service).toBeDefined();
137
- });
138
-
139
- // ============================================================================
140
- // registerProvider() Method
141
- // ============================================================================
142
-
143
- describe('registerProvider', () => {
144
- it('should register provider successfully', () => {
145
- service.registerProvider(mockProvider1);
146
-
147
- expect(service.hasProvider('totp')).toBe(true);
148
- });
149
-
150
- it('should throw error when provider already registered', () => {
151
- service.registerProvider(mockProvider1);
152
-
153
- expect(() => service.registerProvider(mockProvider1)).toThrow(NAuthException);
154
- expect(() => service.registerProvider(mockProvider1)).toThrow('already registered');
155
- });
156
-
157
- it('should allow multiple different providers', () => {
158
- service.registerProvider(mockProvider1);
159
- service.registerProvider(mockProvider2);
160
-
161
- expect(service.hasProvider('totp')).toBe(true);
162
- expect(service.hasProvider('sms')).toBe(true);
163
- });
164
- });
165
-
166
- // ============================================================================
167
- // getProvider() Method
168
- // ============================================================================
169
-
170
- describe('getProvider', () => {
171
- it('should return registered provider', () => {
172
- service.registerProvider(mockProvider1);
173
-
174
- const provider = service.getProvider('totp');
175
-
176
- expect(provider).toBe(mockProvider1);
177
- });
178
-
179
- it('should throw error when provider not registered', () => {
180
- expect(() => service.getProvider('totp')).toThrow(NAuthException);
181
- expect(() => service.getProvider('totp')).toThrow('not registered');
182
- });
183
- });
184
-
185
- // ============================================================================
186
- // hasProvider() Method
187
- // ============================================================================
188
-
189
- describe('hasProvider', () => {
190
- it('should return true for registered provider', () => {
191
- service.registerProvider(mockProvider1);
192
-
193
- expect(service.hasProvider('totp')).toBe(true);
194
- });
195
-
196
- it('should return false for unregistered provider', () => {
197
- expect(service.hasProvider('totp')).toBe(false);
198
- });
199
- });
200
-
201
- // ============================================================================
202
- // listProviders() Method
203
- // ============================================================================
204
-
205
- describe('listProviders', () => {
206
- it('should return empty array when no providers registered', () => {
207
- expect(service.listProviders()).toEqual([]);
208
- });
209
-
210
- it('should return all registered provider names', () => {
211
- service.registerProvider(mockProvider1);
212
- service.registerProvider(mockProvider2);
213
-
214
- const providers = service.listProviders();
215
-
216
- expect(providers).toContain('totp');
217
- expect(providers).toContain('sms');
218
- expect(providers.length).toBe(2);
219
- });
220
- });
221
-
222
- // ============================================================================
223
- // getAvailableMethods() Method
224
- // ============================================================================
225
-
226
- describe('getAvailableMethods', () => {
227
- it('should return only allowed methods', async () => {
228
- service.registerProvider(mockProvider1);
229
- service.registerProvider(mockProvider2);
230
-
231
- const methods = await service.getAvailableMethods(mockUser as IUser);
232
-
233
- expect(methods).toContain('totp');
234
- expect(methods).toContain('sms');
235
- });
236
-
237
- it('should filter out methods not allowed by provider', async () => {
238
- const restrictedProvider = {
239
- ...mockProvider1,
240
- isMethodAllowed: jest.fn().mockReturnValue(false),
241
- };
242
-
243
- service.registerProvider(restrictedProvider);
244
-
245
- const methods = await service.getAvailableMethods(mockUser as IUser);
246
-
247
- expect(methods).not.toContain('totp');
248
- });
249
-
250
- it('should return empty array when no providers registered', async () => {
251
- const methods = await service.getAvailableMethods(mockUser as IUser);
252
-
253
- expect(methods).toEqual([]);
254
- });
255
- });
256
-
257
- // ============================================================================
258
- // verifyCode() Method
259
- // ============================================================================
260
-
261
- describe('verifyCode', () => {
262
- beforeEach(() => {
263
- service.registerProvider(mockProvider1);
264
- });
265
-
266
- it('should route verification to correct provider', async () => {
267
- mockProvider1.verify.mockResolvedValue(true);
268
-
269
- const result = await service.verifyCode(mockUser as IUser, 'totp', '123456');
270
-
271
- expect(result).toBe(true);
272
- expect(mockProvider1.verify).toHaveBeenCalledWith(mockUser as IUser, '123456', undefined);
273
- });
274
-
275
- it('should throw error when provider not registered', async () => {
276
- try {
277
- await service.verifyCode(mockUser as IUser, 'sms', '123456');
278
- fail('Should have thrown NAuthException');
279
- } catch (error: any) {
280
- expect(error).toBeInstanceOf(NAuthException);
281
- }
282
- });
283
-
284
- it('should handle backup code verification', async () => {
285
- // Create a new service instance to avoid provider already registered error
286
- const serviceForBackupTest = new MFAService(
287
- mockMfaDeviceRepository,
288
- mockUserRepository,
289
- mockChallengeService,
290
- mockConfig as NAuthConfig,
291
- mockLogger,
292
- mockAuditService,
293
- mockClientInfoService,
294
- );
295
-
296
- const providerWithBackup = {
297
- ...mockProvider1,
298
- verifyBackupCode: jest.fn().mockResolvedValue(true),
299
- };
300
-
301
- serviceForBackupTest.registerProvider(providerWithBackup);
302
-
303
- const result = await serviceForBackupTest.verifyCode(mockUser as IUser, MFAMethod.BACKUP, 'ABC12345');
304
-
305
- expect(result).toBe(true);
306
- expect(providerWithBackup.verifyBackupCode).toHaveBeenCalledWith(mockUser as IUser, 'ABC12345');
307
- });
308
-
309
- it('should throw error when backup code verification not available', async () => {
310
- try {
311
- await service.verifyCode(mockUser as IUser, MFAMethod.BACKUP, 'ABC12345');
312
- fail('Should have thrown NAuthException');
313
- } catch (error: any) {
314
- expect(error).toBeInstanceOf(NAuthException);
315
- expect(error.message).toContain('Backup code verification not available');
316
- }
317
- });
318
-
319
- it('should pass deviceId to provider', async () => {
320
- mockProvider1.verify.mockResolvedValue(true);
321
-
322
- await service.verifyCode(mockUser as IUser, 'totp', '123456', 1);
323
-
324
- expect(mockProvider1.verify).toHaveBeenCalledWith(mockUser as IUser, '123456', 1);
325
- });
326
- });
327
-
328
- // ============================================================================
329
- // setup() Method
330
- // ============================================================================
331
-
332
- describe('setup', () => {
333
- beforeEach(() => {
334
- service.registerProvider(mockProvider1);
335
- });
336
-
337
- it('should route setup to correct provider', async () => {
338
- const setupData = { secret: 'test-secret', qrCode: 'data:image/png;base64,...' };
339
- mockProvider1.setup.mockResolvedValue(setupData);
340
-
341
- const result = await service.setup(mockUser as IUser, 'totp');
342
-
343
- expect(result).toEqual(setupData);
344
- expect(mockProvider1.setup).toHaveBeenCalledWith(mockUser as IUser, undefined);
345
- });
346
-
347
- it('should throw error when provider not registered', async () => {
348
- try {
349
- await service.setup(mockUser as IUser, 'sms');
350
- fail('Should have thrown NAuthException');
351
- } catch (error: any) {
352
- expect(error).toBeInstanceOf(NAuthException);
353
- }
354
- });
355
-
356
- it('should pass setupData to provider', async () => {
357
- const setupData = { secret: 'test-secret' };
358
- mockProvider1.setup.mockResolvedValue(setupData);
359
-
360
- await service.setup(mockUser as IUser, 'totp', { phoneNumber: '+1234567890' });
361
-
362
- expect(mockProvider1.setup).toHaveBeenCalledWith(mockUser as IUser, { phoneNumber: '+1234567890' });
363
- });
364
- });
365
-
366
- // ============================================================================
367
- // getUserDevices() Method
368
- // ============================================================================
369
-
370
- describe('getUserDevices', () => {
371
- it('should return user devices ordered by primary and creation date', async () => {
372
- const devices = [
373
- { ...mockDevice, id: 1, isPrimary: true },
374
- { ...mockDevice, id: 2, isPrimary: false },
375
- ];
376
-
377
- mockMfaDeviceRepository.find.mockResolvedValue(devices as any);
378
-
379
- const result = await service.getUserDevices(1);
380
-
381
- expect(result).toEqual(devices as any);
382
- expect(mockMfaDeviceRepository.find).toHaveBeenCalledWith({
383
- where: { userId: 1 },
384
- order: { isPrimary: 'DESC', createdAt: 'DESC' },
385
- });
386
- });
387
-
388
- it('should return empty array when no devices found', async () => {
389
- mockMfaDeviceRepository.find.mockResolvedValue([]);
390
-
391
- const result = await service.getUserDevices(1);
392
-
393
- expect(result).toEqual([]);
394
- });
395
- });
396
-
397
- // ============================================================================
398
- // removeDevices() Method
399
- // ============================================================================
400
-
401
- describe('removeDevices', () => {
402
- beforeEach(() => {
403
- service.registerProvider(mockProvider1);
404
- });
405
-
406
- it('should remove devices of specified method type', async () => {
407
- const userEntity = { ...mockUser, id: 1, preferredMfaMethod: 'totp' };
408
- const devices = [
409
- { ...mockDevice, id: 1, type: 'totp' as MFADeviceMethod, isActive: true },
410
- { ...mockDevice, id: 2, type: 'sms' as MFADeviceMethod, isActive: true },
411
- ];
412
-
413
- mockUserRepository.findOne.mockResolvedValue(userEntity as any);
414
- mockMfaDeviceRepository.find
415
- .mockResolvedValueOnce(devices as any) // getUserDevices call
416
- .mockResolvedValueOnce([devices[1]] as any); // After deletion
417
- mockMfaDeviceRepository.delete.mockResolvedValue({ affected: 1 } as any);
418
- mockUserRepository.save.mockResolvedValue(userEntity as any);
419
- mockAuditService.recordEvent.mockResolvedValue({} as any);
420
-
421
- const result = await service.removeDevices('user-uuid-123', 'totp');
422
-
423
- expect(result.deletedCount).toBe(1);
424
- expect(result.mfaDisabled).toBe(false);
425
- expect(mockMfaDeviceRepository.delete).toHaveBeenCalledWith(1);
426
- });
427
-
428
- it('should throw error when method type is invalid', async () => {
429
- try {
430
- await service.removeDevices('user-uuid-123', 'invalid');
431
- fail('Should have thrown NAuthException');
432
- } catch (error: any) {
433
- expect(error).toBeInstanceOf(NAuthException);
434
- expect(error.message).toContain('Invalid MFA method');
435
- }
436
- });
437
-
438
- it('should throw error when user not found', async () => {
439
- mockUserRepository.findOne.mockResolvedValue(null);
440
-
441
- try {
442
- await service.removeDevices('non-existent-user', 'totp');
443
- fail('Should have thrown NAuthException');
444
- } catch (error: any) {
445
- expect(error).toBeInstanceOf(NAuthException);
446
- expect(error.message).toContain('User entity not found');
447
- }
448
- });
449
-
450
- it('should throw error when no devices of method type found', async () => {
451
- const userEntity = { ...mockUser, id: 1 };
452
- const devices = [{ ...mockDevice, id: 1, type: 'sms' as MFADeviceMethod, isActive: true }];
453
-
454
- mockUserRepository.findOne.mockResolvedValue(userEntity as any);
455
- mockMfaDeviceRepository.find.mockResolvedValue(devices as any);
456
-
457
- try {
458
- await service.removeDevices('user-uuid-123', 'totp');
459
- fail('Should have thrown NAuthException');
460
- } catch (error: any) {
461
- expect(error).toBeInstanceOf(NAuthException);
462
- expect(error.message).toContain('No active totp MFA devices found');
463
- }
464
- });
465
-
466
- it('should disable MFA when last device removed', async () => {
467
- const userEntity = { ...mockUser, id: 1, mfaEnabled: true, mfaMethods: ['totp'] };
468
- const devices = [{ ...mockDevice, id: 1, type: 'totp' as MFADeviceMethod, isActive: true }];
469
-
470
- mockUserRepository.findOne.mockResolvedValue(userEntity as any);
471
- mockMfaDeviceRepository.find
472
- .mockResolvedValueOnce(devices as any) // getUserDevices call
473
- .mockResolvedValueOnce([]); // After deletion - no devices remain
474
- mockMfaDeviceRepository.delete.mockResolvedValue({ affected: 1 } as any);
475
- mockUserRepository.save.mockResolvedValue(userEntity as any);
476
- mockAuditService.recordEvent.mockResolvedValue({} as any);
477
-
478
- const result = await service.removeDevices('user-uuid-123', 'totp');
479
-
480
- expect(result.mfaDisabled).toBe(true);
481
- expect(userEntity.mfaEnabled).toBe(false);
482
- expect(userEntity.mfaMethods).toEqual([]);
483
- expect(userEntity.preferredMfaMethod).toBeNull();
484
- });
485
-
486
- it('should create MFA_SETUP_REQUIRED challenge when MFA disabled and enforcement is REQUIRED', async () => {
487
- const userEntity = {
488
- ...mockUser,
489
- id: 1,
490
- mfaEnabled: true,
491
- mfaMethods: ['totp'],
492
- };
493
- const devices = [{ ...mockDevice, id: 1, type: 'totp' as MFADeviceMethod, isActive: true }];
494
- const configWithEnforcement: Partial<NAuthConfig> = {
495
- mfa: {
496
- enabled: true,
497
- enforcement: 'REQUIRED',
498
- allowedMethods: [MFAMethod.TOTP as any, MFAMethod.SMS as any],
499
- },
500
- };
501
-
502
- const serviceWithEnforcement = new MFAService(
503
- mockMfaDeviceRepository,
504
- mockUserRepository,
505
- mockChallengeService,
506
- configWithEnforcement as NAuthConfig,
507
- mockLogger,
508
- mockAuditService,
509
- mockClientInfoService,
510
- );
511
-
512
- mockUserRepository.findOne.mockResolvedValue(userEntity as any);
513
- mockMfaDeviceRepository.find.mockResolvedValueOnce(devices as any).mockResolvedValueOnce([]);
514
- mockMfaDeviceRepository.delete.mockResolvedValue({ affected: 1 } as any);
515
- mockUserRepository.save.mockResolvedValue(userEntity as any);
516
- mockAuditService.recordEvent.mockResolvedValue({} as any);
517
- mockChallengeService.createChallengeSession.mockResolvedValue({} as any);
518
-
519
- await serviceWithEnforcement.removeDevices('user-uuid-123', 'totp');
520
-
521
- expect(mockChallengeService.createChallengeSession).toHaveBeenCalledWith(
522
- userEntity as IUser,
523
- AuthChallenge.MFA_SETUP_REQUIRED,
524
- (expect as any).objectContaining({
525
- allowedMethods: ['totp', 'sms'],
526
- requiresSetup: true,
527
- }),
528
- );
529
- });
530
-
531
- it('should update preferred method when removed method was preferred', async () => {
532
- const userEntity = { ...mockUser, id: 1, preferredMfaMethod: 'totp' };
533
- const devices = [
534
- { ...mockDevice, id: 1, type: 'totp' as MFADeviceMethod, isActive: true },
535
- { ...mockDevice, id: 2, type: 'sms' as MFADeviceMethod, isActive: true },
536
- ];
537
-
538
- mockUserRepository.findOne.mockResolvedValue(userEntity as any);
539
- mockMfaDeviceRepository.find.mockResolvedValueOnce(devices as any).mockResolvedValueOnce([devices[1]] as any);
540
- mockMfaDeviceRepository.delete.mockResolvedValue({ affected: 1 } as any);
541
- mockUserRepository.save.mockResolvedValue(userEntity as any);
542
- mockMfaDeviceRepository.update.mockResolvedValue({ affected: 1 } as any);
543
- mockAuditService.recordEvent.mockResolvedValue({} as any);
544
-
545
- await service.removeDevices('user-uuid-123', 'totp');
546
-
547
- expect(userEntity.preferredMfaMethod).toBe('sms');
548
- expect(mockMfaDeviceRepository.update).toHaveBeenCalledWith({ id: 2 } as any, { isPrimary: true } as any);
549
- });
550
-
551
- it('should record audit event when device removed', async () => {
552
- const userEntity = { ...mockUser, id: 1 };
553
- const devices = [{ ...mockDevice, id: 1, type: 'totp' as MFADeviceMethod, isActive: true }];
554
-
555
- mockUserRepository.findOne.mockResolvedValue(userEntity as any);
556
- mockMfaDeviceRepository.find.mockResolvedValueOnce(devices as any).mockResolvedValueOnce([]);
557
- mockMfaDeviceRepository.delete.mockResolvedValue({ affected: 1 } as any);
558
- mockUserRepository.save.mockResolvedValue(userEntity as any);
559
- mockAuditService.recordEvent.mockResolvedValue({} as any);
560
-
561
- await service.removeDevices('user-uuid-123', 'totp');
562
-
563
- expect(mockAuditService.recordEvent).toHaveBeenCalledWith(
564
- (expect as any).objectContaining({
565
- userId: 1,
566
- eventType: AuthAuditEventType.MFA_DISABLED,
567
- eventStatus: 'INFO',
568
- reason: 'all_devices_removed',
569
- }),
570
- );
571
- });
572
- });
573
-
574
- // ============================================================================
575
- // setPreferredMethod() Method
576
- // ============================================================================
577
-
578
- describe('setPreferredMethod', () => {
579
- beforeEach(() => {
580
- service.registerProvider(mockProvider1);
581
- service.registerProvider(mockProvider2);
582
- });
583
-
584
- it('should set preferred method successfully', async () => {
585
- const userEntity = { ...mockUser, id: 1, preferredMfaMethod: 'totp' };
586
- const devices = [
587
- { ...mockDevice, id: 1, type: 'totp' as MFADeviceMethod, isActive: true },
588
- { ...mockDevice, id: 2, type: 'sms' as MFADeviceMethod, isActive: true },
589
- ];
590
-
591
- mockUserRepository.findOne.mockResolvedValue(userEntity as any);
592
- mockMfaDeviceRepository.find.mockResolvedValue(devices as any);
593
- mockUserRepository.update.mockResolvedValue({ affected: 1 } as any);
594
- mockMfaDeviceRepository.update.mockResolvedValue({ affected: 1 } as any);
595
- mockAuditService.recordEvent.mockResolvedValue({} as any);
596
-
597
- await service.setPreferredMethod('user-uuid-123', 'sms');
598
-
599
- expect(mockUserRepository.update).toHaveBeenCalledWith({ id: 1 } as any, { preferredMfaMethod: 'sms' } as any);
600
- expect(mockMfaDeviceRepository.update).toHaveBeenCalledWith({ id: 2 } as any, { isPrimary: true } as any);
601
- });
602
-
603
- it('should throw error when method type is invalid', async () => {
604
- try {
605
- await service.setPreferredMethod('user-uuid-123', 'invalid');
606
- fail('Should have thrown NAuthException');
607
- } catch (error: any) {
608
- expect(error).toBeInstanceOf(NAuthException);
609
- expect(error.message).toContain('Invalid MFA method');
610
- }
611
- });
612
-
613
- it('should throw error when user not found', async () => {
614
- mockUserRepository.findOne.mockResolvedValue(null);
615
-
616
- try {
617
- await service.setPreferredMethod('non-existent-user', 'totp');
618
- fail('Should have thrown NAuthException');
619
- } catch (error: any) {
620
- expect(error).toBeInstanceOf(NAuthException);
621
- expect(error.message).toContain('User not found');
622
- }
623
- });
624
-
625
- it('should throw error when method not configured for user', async () => {
626
- const userEntity = { ...mockUser, id: 1 };
627
- const devices = [{ ...mockDevice, id: 1, type: 'totp' as MFADeviceMethod, isActive: true }];
628
-
629
- mockUserRepository.findOne.mockResolvedValue(userEntity as any);
630
- mockMfaDeviceRepository.find.mockResolvedValue(devices as any);
631
-
632
- try {
633
- await service.setPreferredMethod('user-uuid-123', 'sms');
634
- fail('Should have thrown NAuthException');
635
- } catch (error: any) {
636
- expect(error).toBeInstanceOf(NAuthException);
637
- expect(error.message).toContain('is not configured for this user');
638
- }
639
- });
640
-
641
- it('should update device primary flags correctly', async () => {
642
- const userEntity = { ...mockUser, id: 1 };
643
- const devices = [
644
- { ...mockDevice, id: 1, type: 'totp' as MFADeviceMethod, isActive: true },
645
- { ...mockDevice, id: 2, type: 'sms' as MFADeviceMethod, isActive: true },
646
- { ...mockDevice, id: 3, type: 'sms' as MFADeviceMethod, isActive: true },
647
- ];
648
-
649
- mockUserRepository.findOne.mockResolvedValue(userEntity as any);
650
- mockMfaDeviceRepository.find.mockResolvedValue(devices as any);
651
- mockUserRepository.update.mockResolvedValue({ affected: 1 } as any);
652
- mockMfaDeviceRepository.update.mockResolvedValue({ affected: 1 } as any);
653
- mockAuditService.recordEvent.mockResolvedValue({} as any);
654
-
655
- await service.setPreferredMethod('user-uuid-123', 'sms');
656
-
657
- // Should set device 2 (first SMS device) as primary
658
- expect(mockMfaDeviceRepository.update).toHaveBeenCalledWith({ id: 2 } as any, { isPrimary: true } as any);
659
- // Should unset primary on other devices
660
- expect(mockMfaDeviceRepository.update).toHaveBeenCalledWith({ id: 1 } as any, { isPrimary: false } as any);
661
- expect(mockMfaDeviceRepository.update).toHaveBeenCalledWith({ id: 3 } as any, { isPrimary: false } as any);
662
- });
663
-
664
- it('should record audit event when preferred method updated', async () => {
665
- const userEntity = { ...mockUser, id: 1, preferredMfaMethod: 'totp' };
666
- const devices = [
667
- { ...mockDevice, id: 1, type: 'totp' as MFADeviceMethod, isActive: true },
668
- { ...mockDevice, id: 2, type: 'sms' as MFADeviceMethod, isActive: true },
669
- ];
670
-
671
- mockUserRepository.findOne.mockResolvedValue(userEntity as any);
672
- mockMfaDeviceRepository.find.mockResolvedValue(devices as any);
673
- mockUserRepository.update.mockResolvedValue({ affected: 1 } as any);
674
- mockMfaDeviceRepository.update.mockResolvedValue({ affected: 1 } as any);
675
- mockAuditService.recordEvent.mockResolvedValue({} as any);
676
-
677
- await service.setPreferredMethod('user-uuid-123', 'sms');
678
-
679
- expect(mockAuditService.recordEvent).toHaveBeenCalledWith(
680
- (expect as any).objectContaining({
681
- userId: 1,
682
- eventType: AuthAuditEventType.MFA_PREFERRED_METHOD_UPDATED,
683
- eventStatus: 'INFO',
684
- metadata: (expect as any).objectContaining({
685
- previousMethod: 'totp',
686
- newMethod: 'sms',
687
- deviceId: 2,
688
- }),
689
- }),
690
- );
691
- });
692
- });
693
- });