@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,457 +0,0 @@
1
- import { IUser } from '../interfaces/entities.interface';
2
- import { StorageAdapter } from '../interfaces/storage-adapter.interface';
3
- import { InternalAuthAuditService as AuthAuditService } from './auth-audit.service';
4
- import { AuthAuditEventType } from '../enums/auth-audit-event-type.enum';
5
- import { RiskFactor } from '../enums/risk-factor.enum';
6
- import { RiskDetectionService } from './risk-detection.service';
7
- import { RiskScoringService } from './risk-scoring.service';
8
- import { ClientInfoService } from './client-info.service';
9
- import { ClientInfo } from '../interfaces/client-info.interface';
10
- import { NAuthConfig, AdaptiveMFARiskEventPayload, SignInBlockedPayload } from '../interfaces/config.interface';
11
- import { NAuthLogger } from '../utils/nauth-logger';
12
-
13
- /**
14
- * Adaptive MFA decision result
15
- */
16
- export interface AdaptiveMFADecision {
17
- /**
18
- * Action to take
19
- */
20
- action: 'allow' | 'require_mfa' | 'block_signin';
21
-
22
- /**
23
- * Risk score (0-100)
24
- */
25
- riskScore: number;
26
-
27
- /**
28
- * Risk level classification
29
- */
30
- riskLevel: 'low' | 'medium' | 'high';
31
-
32
- /**
33
- * Detected risk factors
34
- * Array of RiskFactor enum values (stored as strings at runtime)
35
- */
36
- riskFactors: RiskFactor[];
37
-
38
- /**
39
- * Whether user should be notified
40
- */
41
- notifyUser: boolean;
42
-
43
- /**
44
- * Whether lifecycle hook overrode the decision
45
- */
46
- hookOverride: boolean;
47
-
48
- /**
49
- * Risk event payload (included when action requires it or notifyUser is true)
50
- * Contains full client context for use in blockUserSignIn or audit logging
51
- */
52
- payload?: AdaptiveMFARiskEventPayload;
53
- }
54
-
55
- /**
56
- * Adaptive MFA Decision Service
57
- *
58
- * Makes context-aware MFA requirement decisions based on risk analysis.
59
- * Supports multiple actions (allow, require_mfa, block_signin) based on risk level.
60
- *
61
- * **Decision Flow:**
62
- * 1. Detect risk factors (via RiskDetectionService)
63
- * 2. Calculate risk score (via RiskScoringService)
64
- * 3. Determine risk level and action from configuration
65
- * 4. Call lifecycle hooks if notifyUser is true
66
- * 5. Record audit event (non-blocking)
67
- * 6. Return decision object
68
- *
69
- * **Default Risk Levels:**
70
- * - Low (0-20): action 'allow', notifyUser false
71
- * - Medium (21-50): action 'require_mfa', notifyUser true
72
- * - High (51-100): action 'require_mfa', notifyUser true (conservative default)
73
- *
74
- * **User Blocking:**
75
- * When action is 'block_signin', user is blocked in storage adapter with optional TTL.
76
- * Block status is checked before evaluation to prevent blocked users from attempting sign-in.
77
- *
78
- * @example
79
- * ```typescript
80
- * const decision = await adaptiveMFADecisionService.evaluateAdaptiveMFA(user, 'password');
81
- * if (decision.action === 'block_signin') {
82
- * throw new NAuthException(AuthErrorCode.SIGNIN_BLOCKED_HIGH_RISK, 'Sign-in blocked');
83
- * }
84
- * return decision.action === 'require_mfa';
85
- * ```
86
- */
87
- export class AdaptiveMFADecisionService {
88
- /**
89
- * Default risk level configuration
90
- *
91
- * Conservative defaults that prioritize security:
92
- * - Low risk: Allow without MFA (normal flow)
93
- * - Medium risk: Require MFA
94
- * - High risk: Require MFA (conservative - don't block by default)
95
- */
96
- private readonly defaultRiskLevels: {
97
- low: { maxScore: number; action?: 'allow' | 'require_mfa' | 'block_signin'; notifyUser?: boolean };
98
- medium: { maxScore: number; action?: 'allow' | 'require_mfa' | 'block_signin'; notifyUser?: boolean };
99
- high: { maxScore: number; action?: 'allow' | 'require_mfa' | 'block_signin'; notifyUser?: boolean };
100
- } = {
101
- low: {
102
- maxScore: 20,
103
- action: 'allow',
104
- notifyUser: false,
105
- },
106
- medium: {
107
- maxScore: 50,
108
- action: 'require_mfa',
109
- notifyUser: true,
110
- },
111
- high: {
112
- maxScore: 100,
113
- action: 'require_mfa', // Conservative default (don't block)
114
- notifyUser: true,
115
- },
116
- };
117
-
118
- constructor(
119
- private readonly riskDetectionService: RiskDetectionService,
120
- private readonly riskScoringService: RiskScoringService,
121
- private readonly storageAdapter: StorageAdapter,
122
- private readonly clientInfoService: ClientInfoService,
123
- private readonly config: NAuthConfig,
124
- private readonly logger: NAuthLogger,
125
- private readonly auditService?: AuthAuditService, // Optional - audit trail service (enabled via config.auditLogs.enabled)
126
- ) {}
127
-
128
- /**
129
- * Evaluate adaptive MFA requirement with risk-based actions
130
- *
131
- * Main entry point for adaptive MFA evaluation. Analyzes current login context,
132
- * calculates risk score, determines action, and calls lifecycle hooks.
133
- *
134
- * @param user - User being authenticated
135
- * @param authMethod - Authentication method ('password', 'google', 'apple', etc.)
136
- * @returns Decision object with action, risk details, and hook override status
137
- *
138
- * @example
139
- * ```typescript
140
- * const decision = await adaptiveMFADecisionService.evaluateAdaptiveMFA(user, 'password');
141
- * if (decision.action === 'block_signin') {
142
- * // Handle blocking
143
- * }
144
- * ```
145
- */
146
- async evaluateAdaptiveMFA(user: IUser, authMethod: string): Promise<AdaptiveMFADecision> {
147
- // Validate email is present (required by IUser interface but runtime check for safety)
148
- if (!user.email) {
149
- this.logger?.error?.(`User ${user.sub} missing email - cannot evaluate adaptive MFA`, {
150
- userId: user.id,
151
- userSub: user.sub,
152
- });
153
- throw new Error(`User email is required for adaptive MFA evaluation`);
154
- }
155
-
156
- // Get current client context
157
- const clientInfo: ClientInfo = this.clientInfoService.get();
158
-
159
- // Detect risk factors
160
- const riskFactors = await this.riskDetectionService.detectRiskFactors(user, clientInfo);
161
-
162
- // Calculate risk score
163
- const riskScore = this.riskScoringService.calculateRiskScore(riskFactors);
164
-
165
- // Determine risk level and action
166
- const riskLevels = this.config.mfa?.adaptive?.riskLevels || this.defaultRiskLevels;
167
- const { level, action, notifyUser } = this.determineRiskLevelAndAction(riskScore, riskLevels);
168
-
169
- this.logger?.log?.(
170
- `Adaptive MFA evaluation: user=${user.sub}, score=${riskScore}, level=${level}, action=${action}, notify=${notifyUser}, factors=[${riskFactors.join(', ')}]`,
171
- );
172
-
173
- // Prepare payload for hooks and audit
174
- // Include payload when action requires it (block_signin) or when notifyUser is true
175
- const payload: AdaptiveMFARiskEventPayload = {
176
- user: {
177
- sub: user.sub,
178
- email: user.email, // Safe after validation above
179
- username: user.username || undefined,
180
- phoneNumber: user.phone || undefined,
181
- },
182
- riskScore,
183
- riskLevel: level,
184
- riskFactors,
185
- action,
186
- clientInfo: {
187
- ipAddress: clientInfo.ipAddress,
188
- ipCountry: clientInfo.ipCountry,
189
- ipCity: clientInfo.ipCity,
190
- deviceId: clientInfo.deviceToken, // deviceToken maps to deviceId in sessions
191
- deviceName: clientInfo.deviceName,
192
- deviceType: clientInfo.deviceType,
193
- userAgent: clientInfo.userAgent,
194
- platform: clientInfo.platform,
195
- browser: clientInfo.browser,
196
- },
197
- authMethod,
198
- timestamp: new Date(),
199
- };
200
-
201
- // Call lifecycle hook if configured and user should be notified
202
- let hookOverride = false;
203
- if (notifyUser && this.config.hooks?.onAdaptiveMFATriggered) {
204
- try {
205
- const result = await this.config.hooks.onAdaptiveMFATriggered(payload);
206
- // Hook can return false to override and allow sign-in
207
- if (result === false) {
208
- hookOverride = true;
209
- this.logger?.warn?.(`Adaptive MFA action overridden by hook: user=${user.sub}`);
210
- }
211
- } catch (error) {
212
- // Non-blocking: Log error but continue with original action
213
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
214
- this.logger?.error?.(`Adaptive MFA hook failed: ${errorMessage}`, { error, userId: user.sub });
215
- }
216
- }
217
-
218
- // Record in audit trail (non-blocking)
219
- // This logs the risk assessment result
220
- // Determine event status based on risk level and action:
221
- // - block_signin: SUSPICIOUS (security violation)
222
- // - require_mfa with high/medium risk or suspicious factors: SUSPICIOUS
223
- // - require_mfa with low risk: INFO (normal security measure)
224
- // - allow: INFO (no risk detected)
225
- const hasSuspiciousFactors =
226
- riskFactors.includes(RiskFactor.SUSPICIOUS_ACTIVITY) ||
227
- riskFactors.includes(RiskFactor.IMPOSSIBLE_TRAVEL) ||
228
- level === 'high';
229
- const eventStatus =
230
- action === 'block_signin'
231
- ? 'SUSPICIOUS'
232
- : action === 'require_mfa' && hasSuspiciousFactors
233
- ? 'SUSPICIOUS'
234
- : 'INFO';
235
-
236
- this.auditService
237
- ?.recordEvent({
238
- userId: user.id,
239
- eventType: AuthAuditEventType.ADAPTIVE_MFA_RISK_ASSESSED,
240
- eventStatus,
241
- riskFactor: riskScore,
242
- riskFactors,
243
- adaptiveMfaTriggered: action !== 'allow',
244
- description: `Adaptive MFA risk assessment: ${action} (score: ${riskScore}, level: ${level})`,
245
- authMethod,
246
- metadata: {
247
- riskScore,
248
- riskLevel: level,
249
- action,
250
- riskFactors,
251
- },
252
- // Client info automatically included from context
253
- })
254
- .catch((err) => {
255
- this.logger?.warn?.(`Failed to record ADAPTIVE_MFA_RISK_ASSESSED audit: ${err.message}`);
256
- });
257
-
258
- return {
259
- action: hookOverride ? 'allow' : action,
260
- riskScore,
261
- riskLevel: level,
262
- riskFactors,
263
- notifyUser,
264
- hookOverride,
265
- // Include payload when action requires it or when notifyUser is true
266
- // This ensures consistent clientInfo data for blockUserSignIn and audit logs
267
- payload: action === 'block_signin' || notifyUser ? payload : undefined,
268
- };
269
- }
270
-
271
- /**
272
- * Determine risk level and action based on score and configured thresholds
273
- *
274
- * Evaluates risk score against configured thresholds in order: low → medium → high.
275
- * Returns the first level that the score falls within.
276
- *
277
- * @param riskScore - Calculated risk score (0-100)
278
- * @param riskLevels - Configured risk level thresholds
279
- * @returns Risk level, action, and notifyUser flag
280
- * @private
281
- */
282
- private determineRiskLevelAndAction(
283
- riskScore: number,
284
- riskLevels: {
285
- low?: { maxScore: number; action?: 'allow' | 'require_mfa' | 'block_signin'; notifyUser?: boolean };
286
- medium?: { maxScore: number; action?: 'allow' | 'require_mfa' | 'block_signin'; notifyUser?: boolean };
287
- high?: { maxScore: number; action?: 'allow' | 'require_mfa' | 'block_signin'; notifyUser?: boolean };
288
- },
289
- ): {
290
- level: 'low' | 'medium' | 'high';
291
- action: 'allow' | 'require_mfa' | 'block_signin';
292
- notifyUser: boolean;
293
- } {
294
- // Check in order: low → medium → high
295
- if (riskScore <= (riskLevels.low?.maxScore ?? 20)) {
296
- return {
297
- level: 'low',
298
- action: riskLevels.low?.action ?? 'allow',
299
- notifyUser: riskLevels.low?.notifyUser ?? false,
300
- };
301
- }
302
-
303
- if (riskScore <= (riskLevels.medium?.maxScore ?? 50)) {
304
- return {
305
- level: 'medium',
306
- action: riskLevels.medium?.action ?? 'require_mfa',
307
- notifyUser: riskLevels.medium?.notifyUser ?? true,
308
- };
309
- }
310
-
311
- return {
312
- level: 'high',
313
- action: riskLevels.high?.action ?? 'require_mfa',
314
- notifyUser: riskLevels.high?.notifyUser ?? true,
315
- };
316
- }
317
-
318
- /**
319
- * Check if user is currently blocked due to high-risk sign-in
320
- *
321
- * Uses storage adapter to check for existing block. Block is stored with
322
- * key format: `adaptive_mfa_block:{userId}`.
323
- *
324
- * @param userId - Internal user ID (integer)
325
- * @returns Block status with expiration and message if blocked
326
- *
327
- * @example
328
- * ```typescript
329
- * const blockStatus = await adaptiveMFADecisionService.isUserBlocked(user.id);
330
- * if (blockStatus.blocked) {
331
- * throw new NAuthException(AuthErrorCode.SIGNIN_BLOCKED_HIGH_RISK, blockStatus.message);
332
- * }
333
- * ```
334
- */
335
- async isUserBlocked(userId: number): Promise<{
336
- blocked: boolean;
337
- expiresAt?: Date;
338
- message?: string;
339
- }> {
340
- try {
341
- const blockKey = `adaptive_mfa_block:${userId}`;
342
- const blockData = await this.storageAdapter.get(blockKey);
343
-
344
- if (!blockData) {
345
- return { blocked: false };
346
- }
347
-
348
- const parsed = JSON.parse(blockData);
349
- const expiresAt = parsed.expiresAt ? new Date(parsed.expiresAt) : undefined;
350
-
351
- // Check if block has expired (if temporary)
352
- if (expiresAt && expiresAt < new Date()) {
353
- // Block expired - clean up
354
- await this.storageAdapter.del(blockKey);
355
- return { blocked: false };
356
- }
357
-
358
- return {
359
- blocked: true,
360
- expiresAt,
361
- message: parsed.message,
362
- };
363
- } catch (error) {
364
- // Non-blocking: Log error but assume not blocked (safer for UX)
365
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
366
- this.logger?.warn?.(`Failed to check user block status: ${errorMessage}`, { error, userId });
367
- return { blocked: false };
368
- }
369
- }
370
-
371
- /**
372
- * Block user sign-in due to high risk
373
- *
374
- * Stores block in storage adapter with optional TTL. Block data includes:
375
- * - userId, userSub (for reference)
376
- * - message (shown to user)
377
- * - riskScore, riskFactors (for audit)
378
- * - blockedAt, expiresAt (timestamps)
379
- *
380
- * Also calls onSignInBlocked lifecycle hook if configured.
381
- *
382
- * @param user - User to block
383
- * @param payload - Risk event payload with all context
384
- *
385
- * @example
386
- * ```typescript
387
- * await adaptiveMFADecisionService.blockUserSignIn(user, payload);
388
- * ```
389
- */
390
- async blockUserSignIn(user: IUser, payload: AdaptiveMFARiskEventPayload): Promise<void> {
391
- const blockConfig = this.config.mfa?.adaptive?.blockedSignIn;
392
- const blockDuration = blockConfig?.blockDuration; // minutes
393
- const message = blockConfig?.message || 'Sign-in blocked due to suspicious activity. Please contact support.';
394
-
395
- // Store block in storage adapter
396
- const blockKey = `adaptive_mfa_block:${user.id}`;
397
- const blockData = {
398
- userId: user.id,
399
- userSub: user.sub,
400
- message,
401
- riskScore: payload.riskScore,
402
- riskFactors: payload.riskFactors,
403
- blockedAt: new Date().toISOString(),
404
- expiresAt: blockDuration ? new Date(Date.now() + blockDuration * 60 * 1000).toISOString() : undefined,
405
- };
406
-
407
- const ttl = blockDuration ? blockDuration * 60 : undefined; // Convert to seconds
408
- await this.storageAdapter.set(blockKey, JSON.stringify(blockData), ttl);
409
-
410
- this.logger?.warn?.(
411
- `User sign-in blocked: user=${user.sub}, score=${payload.riskScore}, duration=${blockDuration ? `${blockDuration}min` : 'permanent'}`,
412
- );
413
-
414
- // Call sign-in blocked hook if configured
415
- if (this.config.hooks?.onSignInBlocked) {
416
- const blockedPayload: SignInBlockedPayload = {
417
- ...payload,
418
- blockDuration,
419
- blockExpiresAt: blockDuration ? new Date(Date.now() + blockDuration * 60 * 1000) : undefined,
420
- message,
421
- };
422
-
423
- try {
424
- await this.config.hooks.onSignInBlocked(blockedPayload);
425
- } catch (error) {
426
- // Non-blocking
427
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
428
- this.logger?.error?.(`Sign-in blocked hook failed: ${errorMessage}`, { error, userId: user.sub });
429
- }
430
- }
431
- }
432
-
433
- /**
434
- * Clear user block (manual unblock)
435
- *
436
- * Removes the block from storage adapter, allowing user to sign in again.
437
- * Useful for admin actions or when risk situation has improved.
438
- *
439
- * @param userId - Internal user ID (integer)
440
- *
441
- * @example
442
- * ```typescript
443
- * await adaptiveMFADecisionService.clearUserBlock(user.id);
444
- * ```
445
- */
446
- async clearUserBlock(userId: number): Promise<void> {
447
- try {
448
- const blockKey = `adaptive_mfa_block:${userId}`;
449
- await this.storageAdapter.del(blockKey);
450
- this.logger?.log?.(`User block cleared: userId=${userId}`);
451
- } catch (error) {
452
- // Non-blocking: Log error but continue
453
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
454
- this.logger?.warn?.(`Failed to clear user block: ${errorMessage}`, { error, userId });
455
- }
456
- }
457
- }