@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,520 +0,0 @@
1
- import { IUser } from '../interfaces/entities.interface';
2
- import { NAuthConfig } from '../interfaces/config.interface';
3
- import { TrustedDeviceService } from './trusted-device.service';
4
- import { AdaptiveMFADecisionService } from './adaptive-mfa-decision.service';
5
- import { ClientInfoService } from './client-info.service';
6
- import { NAuthLogger } from '../utils/nauth-logger';
7
- import { AuthFlowContext } from './auth-flow-state-machine.types';
8
-
9
- /**
10
- * Authentication Flow Context Builder
11
- *
12
- * Pre-computes all derived values needed for state machine rule evaluation.
13
- * This optimization ensures values are calculated once at the beginning of the flow,
14
- * rather than repeatedly during rule evaluation.
15
- *
16
- * @example
17
- * ```typescript
18
- * const context = await contextBuilder.build({
19
- * user,
20
- * config,
21
- * authMethod: 'password',
22
- * deviceToken: 'abc123'
23
- * });
24
- * ```
25
- */
26
- export class AuthFlowContextBuilder {
27
- constructor(
28
- private readonly trustedDeviceService?: TrustedDeviceService,
29
- private readonly adaptiveMFADecisionService?: AdaptiveMFADecisionService,
30
- _clientInfoService?: ClientInfoService, // Reserved for future use (not stored as property)
31
- private readonly logger?: NAuthLogger,
32
- ) {}
33
-
34
- /**
35
- * Build authentication flow context with pre-computed values
36
- *
37
- * @param params - Context parameters
38
- * @param params.user - User attempting authentication
39
- * @param params.config - Authentication configuration
40
- * @param params.authMethod - Authentication method ('password' or 'social')
41
- * @param params.authProvider - Social auth provider name (e.g., 'google', 'apple')
42
- * @param params.deviceToken - Device token for trusted device check
43
- * @param params.skipMFAVerification - Skip MFA verification flag
44
- * @returns Authentication flow context with computed values
45
- *
46
- * @example
47
- * ```typescript
48
- * const context = await contextBuilder.build({
49
- * user,
50
- * config,
51
- * authMethod: 'password',
52
- * deviceToken: 'abc123'
53
- * });
54
- * ```
55
- */
56
- async build(params: {
57
- user: IUser;
58
- config: NAuthConfig;
59
- authMethod?: 'password' | 'social';
60
- authProvider?: string;
61
- deviceToken?: string;
62
- skipMFAVerification?: boolean;
63
- }): Promise<AuthFlowContext> {
64
- const { user, config, authMethod, authProvider, deviceToken, skipMFAVerification } = params;
65
-
66
- this.logger?.debug?.(
67
- `[ContextBuilder] Building context for user ${user.sub} (authMethod=${authMethod || 'password'}, mfaEnabled=${user.mfaEnabled}, mfaExempt=${user.mfaExempt || false})`,
68
- );
69
-
70
- // ============================================================================
71
- // Pre-compute all derived values
72
- // ============================================================================
73
-
74
- const isEmailVerificationRequired = this.isEmailVerificationRequired(user, config, authMethod);
75
- const isPhoneVerificationRequired = this.isPhoneVerificationRequired(user, config, authMethod);
76
- const isPhoneCollectionNeeded = this.isPhoneCollectionNeeded(user, config, authMethod);
77
- const isMFAExempt = this.checkMFAExempt(user);
78
- const isMFASetupRequired = this.isMFASetupRequired(user, config, authMethod);
79
- const isDeviceTrusted = await this.checkDeviceTrust(user, deviceToken, config);
80
- const gracePeriodData = this.calculateGracePeriod(user, config);
81
- const blockData = await this.checkBlocked(user);
82
- const mfaVerificationData = await this.checkMFAVerification(
83
- user,
84
- config,
85
- authMethod,
86
- deviceToken,
87
- isDeviceTrusted,
88
- skipMFAVerification,
89
- );
90
-
91
- // Merge block status from existing storage and adaptive MFA decision
92
- const isBlocked = blockData.blocked || (mfaVerificationData.isBlocked ?? false);
93
- const blockedUntil = blockData.until; // From existing block
94
- const blockReason = blockData.reason || (mfaVerificationData.isBlocked ? 'Sign in blocked due to suspicious activity' : undefined);
95
-
96
- const computed = {
97
- isEmailVerificationRequired,
98
- isPhoneVerificationRequired,
99
- isPhoneCollectionNeeded,
100
- isMFAExempt,
101
- isMFASetupRequired,
102
- isMFAVerificationRequired: mfaVerificationData.required,
103
- isDeviceTrusted,
104
- isGracePeriodActive: gracePeriodData.isActive,
105
- gracePeriodEndsAt: gracePeriodData.endsAt,
106
- isBlocked,
107
- blockedUntil,
108
- blockReason,
109
- riskScore: mfaVerificationData.riskScore,
110
- riskLevel: mfaVerificationData.riskLevel,
111
- };
112
-
113
- this.logger?.debug?.(
114
- `[ContextBuilder] Computed values: emailReq=${computed.isEmailVerificationRequired}, phoneReq=${computed.isPhoneVerificationRequired}, phoneCollect=${computed.isPhoneCollectionNeeded}, mfaExempt=${computed.isMFAExempt}, mfaSetupReq=${computed.isMFASetupRequired}, mfaVerifyReq=${computed.isMFAVerificationRequired}, trusted=${computed.isDeviceTrusted}, gracePeriod=${computed.isGracePeriodActive}, blocked=${computed.isBlocked}`,
115
- );
116
-
117
- return {
118
- user,
119
- config,
120
- authMethod,
121
- authProvider,
122
- deviceToken,
123
- skipMFAVerification,
124
- computed,
125
- };
126
- }
127
-
128
- /**
129
- * Check if email verification is required
130
- *
131
- * @param user - User to check
132
- * @param config - Auth configuration
133
- * @param authMethod - Authentication method
134
- * @returns True if email verification is required
135
- */
136
- private isEmailVerificationRequired(user: IUser, config: NAuthConfig, authMethod?: 'password' | 'social'): boolean {
137
- const verificationMethod = config.signup?.verificationMethod || 'email';
138
-
139
- // Email verification not required if verification is disabled
140
- if (verificationMethod === 'none' || verificationMethod === 'phone') {
141
- return false;
142
- }
143
-
144
- // Social auth users have email pre-verified by OAuth provider
145
- if (authMethod === 'social') {
146
- return false;
147
- }
148
-
149
- // Check if email is already verified
150
- if (user.isEmailVerified) {
151
- return false;
152
- }
153
-
154
- // Email verification required for 'email' or 'both' methods
155
- return verificationMethod === 'email' || verificationMethod === 'both';
156
- }
157
-
158
- /**
159
- * Check if phone verification is required
160
- *
161
- * @param user - User to check
162
- * @param config - Auth configuration
163
- * @param authMethod - Authentication method
164
- * @returns True if phone verification is required
165
- */
166
- private isPhoneVerificationRequired(user: IUser, config: NAuthConfig, _authMethod?: 'password' | 'social'): boolean {
167
- const verificationMethod = config.signup?.verificationMethod || 'email';
168
-
169
- // Phone verification not required if verification is disabled or email-only
170
- if (verificationMethod === 'none' || verificationMethod === 'email') {
171
- return false;
172
- }
173
-
174
- // Phone verification required for 'phone' or 'both' methods
175
- // But only if user has a phone number
176
- if (verificationMethod === 'phone' || verificationMethod === 'both') {
177
- // If user has no phone, phone collection is needed first (handled separately)
178
- if (!user.phone) {
179
- return false; // Phone collection needed, not verification
180
- }
181
-
182
- // Check if phone is already verified
183
- return !user.isPhoneVerified;
184
- }
185
-
186
- return false;
187
- }
188
-
189
- /**
190
- * Check if phone collection is needed
191
- *
192
- * Phone collection is the step where we ask users to provide their phone number.
193
- * This should NOT be triggered if:
194
- * - User already has a verified phone (e.g., from prior signup or account linking)
195
- * - Phone verification is not required by config
196
- *
197
- * **Bug Fix (2025-12-08):**
198
- * Previously didn't check `isPhoneVerified`, causing social login users with
199
- * verified phones to be asked for phone collection again after account linking.
200
- *
201
- * @param user - User to check
202
- * @param config - Auth configuration
203
- * @param _authMethod - Authentication method (unused, kept for API consistency)
204
- * @returns True if phone collection is needed
205
- */
206
- private isPhoneCollectionNeeded(user: IUser, config: NAuthConfig, _authMethod?: 'password' | 'social'): boolean {
207
- const verificationMethod = config.signup?.verificationMethod || 'email';
208
-
209
- // Phone collection not needed if verification is disabled or email-only
210
- if (verificationMethod === 'none' || verificationMethod === 'email') {
211
- return false;
212
- }
213
-
214
- // ============================================================================
215
- // Skip phone collection if phone is already verified
216
- // ============================================================================
217
- // This handles cases like:
218
- // - User signs up with password + phone verification, then later links social account
219
- // - Account linking where existing account has verified phone
220
- // - Any scenario where phone is already verified (we trust it)
221
- if (user.isPhoneVerified) {
222
- return false;
223
- }
224
-
225
- // Phone collection needed for 'phone' or 'both' methods if user has no phone
226
- if ((verificationMethod === 'phone' || verificationMethod === 'both') && !user.phone) {
227
- return true;
228
- }
229
-
230
- return false;
231
- }
232
-
233
- /**
234
- * Check if user is exempt from MFA
235
- *
236
- * @param user - User to check
237
- * @returns True if user is exempt from MFA
238
- */
239
- private checkMFAExempt(user: IUser): boolean {
240
- const mfaExempt = user.mfaExempt;
241
- // Handle different database representations (boolean true, MySQL tinyint 1, etc.)
242
- return mfaExempt === true || (mfaExempt as unknown) === 1;
243
- }
244
-
245
- /**
246
- * Check if MFA setup is required
247
- *
248
- * @param user - User to check
249
- * @param config - Auth configuration
250
- * @param authMethod - Authentication method
251
- * @returns True if MFA setup is required
252
- */
253
- private isMFASetupRequired(user: IUser, config: NAuthConfig, authMethod?: 'password' | 'social'): boolean {
254
- // Check exemption first
255
- if (this.checkMFAExempt(user)) {
256
- return false;
257
- }
258
-
259
- // MFA not enabled in config
260
- if (!config.mfa?.enabled) {
261
- return false;
262
- }
263
-
264
- // User already has MFA enabled
265
- if (user.mfaEnabled) {
266
- return false;
267
- }
268
-
269
- // Social login exemption
270
- if (authMethod === 'social' && config.mfa.requireForSocialLogin === false) {
271
- return false;
272
- }
273
-
274
- // Check enforcement policy
275
- const enforcement = config.mfa.enforcement || 'OPTIONAL';
276
-
277
- if (enforcement === 'OPTIONAL') {
278
- return false;
279
- }
280
-
281
- // REQUIRED or ADAPTIVE: Check grace period
282
- const gracePeriod = config.mfa.gracePeriod ?? 7;
283
- const gracePeriodData = this.calculateGracePeriod(user, config);
284
-
285
- // If grace period is 0, MFA setup is required immediately
286
- if (gracePeriod === 0) {
287
- return true;
288
- }
289
-
290
- // If grace period is active, MFA setup is optional
291
- if (gracePeriodData.isActive) {
292
- return false;
293
- }
294
-
295
- // Grace period expired - MFA setup required
296
- return true;
297
- }
298
-
299
- /**
300
- * Check if device is trusted
301
- *
302
- * @param user - User to check
303
- * @param deviceToken - Device token
304
- * @param config - Auth configuration
305
- * @returns True if device is trusted
306
- */
307
- private async checkDeviceTrust(user: IUser, deviceToken?: string, config?: NAuthConfig): Promise<boolean> {
308
- if (
309
- !deviceToken ||
310
- !config?.mfa?.rememberDevices ||
311
- config.mfa.rememberDevices === 'never' ||
312
- !this.trustedDeviceService
313
- ) {
314
- return false;
315
- }
316
-
317
- try {
318
- const validation = await this.trustedDeviceService.validateDeviceToken(deviceToken, user.id);
319
- return validation.isValid;
320
- } catch (error) {
321
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
322
- this.logger?.warn?.(`Failed to check device trust: ${errorMessage}`, { error, userId: user.id });
323
- return false;
324
- }
325
- }
326
-
327
- /**
328
- * Calculate grace period status
329
- *
330
- * @param user - User to check
331
- * @param config - Auth configuration
332
- * @returns Grace period status
333
- */
334
- private calculateGracePeriod(user: IUser, config: NAuthConfig): { isActive: boolean; endsAt?: Date } {
335
- const gracePeriod = config.mfa?.gracePeriod ?? 7;
336
-
337
- // No grace period
338
- if (gracePeriod === 0) {
339
- return { isActive: false };
340
- }
341
-
342
- // Access createdAt from user interface
343
- const userWithDates = user as IUser & { createdAt: Date };
344
- const createdAt = userWithDates.createdAt;
345
-
346
- if (!createdAt) {
347
- // No creation date - grace period not active
348
- return { isActive: false };
349
- }
350
-
351
- const gracePeriodEnd = new Date(createdAt);
352
- gracePeriodEnd.setDate(gracePeriodEnd.getDate() + gracePeriod);
353
-
354
- const now = new Date();
355
- const isActive = now < gracePeriodEnd;
356
-
357
- return {
358
- isActive,
359
- endsAt: isActive ? gracePeriodEnd : undefined,
360
- };
361
- }
362
-
363
- /**
364
- * Check if user is blocked
365
- *
366
- * @param user - User to check
367
- * @returns Block status
368
- */
369
- private async checkBlocked(user: IUser): Promise<{ blocked: boolean; until?: Date; reason?: string }> {
370
- if (!this.adaptiveMFADecisionService) {
371
- return { blocked: false };
372
- }
373
-
374
- try {
375
- const blockStatus = await this.adaptiveMFADecisionService.isUserBlocked(user.id);
376
- return {
377
- blocked: blockStatus.blocked,
378
- until: blockStatus.expiresAt,
379
- reason: blockStatus.message,
380
- };
381
- } catch (error) {
382
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
383
- this.logger?.warn?.(`Failed to check user block status: ${errorMessage}`, { error, userId: user.id });
384
- return { blocked: false };
385
- }
386
- }
387
-
388
- /**
389
- * Check if MFA verification is required
390
- *
391
- * @param user - User to check
392
- * @param config - Auth configuration
393
- * @param authMethod - Authentication method
394
- * @param deviceToken - Device token
395
- * @param isDeviceTrusted - Whether device is trusted
396
- * @param skipMFAVerification - Skip MFA verification flag
397
- * @returns MFA verification requirement and risk data
398
- */
399
- private async checkMFAVerification(
400
- user: IUser,
401
- config: NAuthConfig,
402
- authMethod?: 'password' | 'social',
403
- _deviceToken?: string, // Reserved for future use
404
- isDeviceTrusted?: boolean,
405
- skipMFAVerification?: boolean,
406
- ): Promise<{ required: boolean; riskScore?: number; riskLevel?: 'low' | 'medium' | 'high'; isBlocked?: boolean }> {
407
- // Skip if flag is set
408
- if (skipMFAVerification) {
409
- return { required: false };
410
- }
411
-
412
- // Check exemption first
413
- if (this.checkMFAExempt(user)) {
414
- return { required: false };
415
- }
416
-
417
- // MFA not enabled in config
418
- if (!config.mfa?.enabled) {
419
- return { required: false };
420
- }
421
-
422
- // User doesn't have MFA enabled
423
- if (!user.mfaEnabled) {
424
- return { required: false };
425
- }
426
-
427
- // Social login exemption
428
- if (authMethod === 'social' && config.mfa.requireForSocialLogin === false) {
429
- return { required: false };
430
- }
431
-
432
- // Check enforcement policy
433
- const enforcement = config.mfa.enforcement || 'OPTIONAL';
434
-
435
- // ============================================================================
436
- // OPTIONAL Enforcement: Setup is optional, but if user has MFA enabled,
437
- // it must be used (unless trusted device bypass applies)
438
- // ============================================================================
439
- if (enforcement === 'OPTIONAL') {
440
- // OPTIONAL means setup is optional, but once enabled, MFA is required
441
- // Check if trusted device bypass applies
442
- if (
443
- isDeviceTrusted &&
444
- config.mfa.rememberDevices &&
445
- config.mfa.rememberDevices !== 'never' &&
446
- config.mfa.bypassMFAForTrustedDevices === true
447
- ) {
448
- return { required: false };
449
- }
450
- // User has MFA enabled - require it
451
- return { required: true };
452
- }
453
-
454
- // Trusted device bypass (for REQUIRED enforcement, not ADAPTIVE)
455
- if (
456
- enforcement === 'REQUIRED' &&
457
- isDeviceTrusted &&
458
- config.mfa.rememberDevices &&
459
- config.mfa.rememberDevices !== 'never' &&
460
- config.mfa.bypassMFAForTrustedDevices === true
461
- ) {
462
- return { required: false };
463
- }
464
-
465
- // ADAPTIVE enforcement
466
- if (enforcement === 'ADAPTIVE') {
467
- if (!this.adaptiveMFADecisionService) {
468
- // Service not available - fall back to REQUIRED behavior
469
- this.logger?.warn?.(
470
- `ADAPTIVE enforcement enabled but AdaptiveMFADecisionService not available - falling back to REQUIRED behavior for user ${user.sub}`,
471
- );
472
- return { required: true };
473
- }
474
-
475
- // Always evaluate adaptive MFA for complete risk assessment (trusted or untrusted)
476
- try {
477
- const decision = await this.adaptiveMFADecisionService.evaluateAdaptiveMFA(user, authMethod || 'password');
478
-
479
- // Handle block_signin action - block user and store in storage
480
- if (decision.action === 'block_signin') {
481
- if (decision.payload) {
482
- await this.adaptiveMFADecisionService.blockUserSignIn(user, decision.payload);
483
- }
484
- // Mark as blocked so state machine will transition to BLOCKED state
485
- return {
486
- required: false, // Not relevant - will be blocked
487
- riskScore: decision.riskScore,
488
- riskLevel: decision.riskLevel,
489
- isBlocked: true,
490
- };
491
- }
492
-
493
- // For untrusted devices, always require MFA regardless of risk score
494
- // (new devices are inherently riskier and should verify)
495
- if (!isDeviceTrusted) {
496
- return {
497
- required: true,
498
- riskScore: decision.riskScore,
499
- riskLevel: decision.riskLevel,
500
- };
501
- }
502
-
503
- // For trusted devices, use risk-based decision
504
- return {
505
- required: decision.action === 'require_mfa',
506
- riskScore: decision.riskScore,
507
- riskLevel: decision.riskLevel,
508
- };
509
- } catch (error) {
510
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
511
- this.logger?.warn?.(`Failed to evaluate adaptive MFA: ${errorMessage}`, { error, userId: user.id });
512
- // Fall back to requiring MFA on error (safer)
513
- return { required: true };
514
- }
515
- }
516
-
517
- // REQUIRED enforcement
518
- return { required: true };
519
- }
520
- }