@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.
- package/LICENSE +90 -0
- package/README.md +9 -0
- package/package.json +8 -3
- package/jest.config.js +0 -15
- package/jest.setup.ts +0 -6
- package/src/adapters/database-columns.ts +0 -165
- package/src/adapters/express.adapter.ts +0 -385
- package/src/adapters/fastify.adapter.ts +0 -416
- package/src/adapters/index.ts +0 -16
- package/src/adapters/storage.factory.ts +0 -143
- package/src/bootstrap.ts +0 -374
- package/src/dto/auth-challenge.dto.ts +0 -231
- package/src/dto/auth-response.dto.ts +0 -253
- package/src/dto/challenge-response.dto.ts +0 -234
- package/src/dto/change-password-request.dto.ts +0 -50
- package/src/dto/change-password-response.dto.ts +0 -29
- package/src/dto/change-password.dto.ts +0 -57
- package/src/dto/error-response.dto.ts +0 -136
- package/src/dto/get-available-methods.dto.ts +0 -55
- package/src/dto/get-challenge-data-response.dto.ts +0 -28
- package/src/dto/get-challenge-data.dto.ts +0 -69
- package/src/dto/get-client-info.dto.ts +0 -104
- package/src/dto/get-device-token-response.dto.ts +0 -25
- package/src/dto/get-events-by-type.dto.ts +0 -76
- package/src/dto/get-ip-address-response.dto.ts +0 -24
- package/src/dto/get-mfa-status.dto.ts +0 -94
- package/src/dto/get-risk-assessment-history.dto.ts +0 -39
- package/src/dto/get-session-id-response.dto.ts +0 -25
- package/src/dto/get-setup-data-response.dto.ts +0 -31
- package/src/dto/get-setup-data.dto.ts +0 -75
- package/src/dto/get-suspicious-activity.dto.ts +0 -42
- package/src/dto/get-user-agent-response.dto.ts +0 -23
- package/src/dto/get-user-auth-history.dto.ts +0 -95
- package/src/dto/get-user-by-email.dto.ts +0 -61
- package/src/dto/get-user-by-id.dto.ts +0 -46
- package/src/dto/get-user-devices.dto.ts +0 -53
- package/src/dto/get-user-response.dto.ts +0 -17
- package/src/dto/has-provider.dto.ts +0 -56
- package/src/dto/index.ts +0 -57
- package/src/dto/is-trusted-device-response.dto.ts +0 -34
- package/src/dto/list-providers-response.dto.ts +0 -23
- package/src/dto/login.dto.ts +0 -95
- package/src/dto/logout-all-response.dto.ts +0 -24
- package/src/dto/logout-all.dto.ts +0 -65
- package/src/dto/logout-response.dto.ts +0 -25
- package/src/dto/logout.dto.ts +0 -64
- package/src/dto/refresh-token.dto.ts +0 -36
- package/src/dto/remove-devices.dto.ts +0 -85
- package/src/dto/resend-code-response.dto.ts +0 -32
- package/src/dto/resend-code.dto.ts +0 -51
- package/src/dto/reset-password.dto.ts +0 -115
- package/src/dto/respond-challenge.dto.ts +0 -272
- package/src/dto/set-mfa-exemption.dto.ts +0 -112
- package/src/dto/set-must-change-password-response.dto.ts +0 -27
- package/src/dto/set-must-change-password.dto.ts +0 -46
- package/src/dto/set-preferred-method.dto.ts +0 -80
- package/src/dto/setup-mfa.dto.ts +0 -98
- package/src/dto/signup.dto.ts +0 -174
- package/src/dto/social-auth.dto.ts +0 -422
- package/src/dto/trust-device-response.dto.ts +0 -30
- package/src/dto/trust-device.dto.ts +0 -9
- package/src/dto/update-user-attributes-request.dto.ts +0 -51
- package/src/dto/user-response.dto.ts +0 -138
- package/src/dto/user-update.dto.ts +0 -222
- package/src/dto/verify-email.dto.ts +0 -313
- package/src/dto/verify-mfa-code.dto.ts +0 -103
- package/src/dto/verify-phone-by-sub.dto.ts +0 -78
- package/src/dto/verify-phone.dto.ts +0 -245
- package/src/entities/auth-audit.entity.ts +0 -232
- package/src/entities/challenge-session.entity.ts +0 -116
- package/src/entities/index.ts +0 -29
- package/src/entities/login-attempt.entity.ts +0 -64
- package/src/entities/mfa-device.entity.ts +0 -151
- package/src/entities/rate-limit.entity.ts +0 -44
- package/src/entities/session.entity.ts +0 -180
- package/src/entities/social-account.entity.ts +0 -96
- package/src/entities/storage-lock.entity.ts +0 -39
- package/src/entities/trusted-device.entity.ts +0 -112
- package/src/entities/user.entity.ts +0 -243
- package/src/entities/verification-token.entity.ts +0 -141
- package/src/enums/auth-audit-event-type.enum.ts +0 -360
- package/src/enums/error-codes.enum.ts +0 -420
- package/src/enums/mfa-method.enum.ts +0 -97
- package/src/enums/risk-factor.enum.ts +0 -111
- package/src/exceptions/nauth.exception.ts +0 -231
- package/src/handlers/auth.handler.ts +0 -260
- package/src/handlers/client-info.handler.ts +0 -101
- package/src/handlers/csrf.handler.ts +0 -156
- package/src/handlers/token-delivery.handler.ts +0 -118
- package/src/index.ts +0 -118
- package/src/interfaces/client-info.interface.ts +0 -85
- package/src/interfaces/config.interface.ts +0 -2135
- package/src/interfaces/entities.interface.ts +0 -226
- package/src/interfaces/index.ts +0 -15
- package/src/interfaces/logger.interface.ts +0 -283
- package/src/interfaces/mfa-provider.interface.ts +0 -154
- package/src/interfaces/oauth.interface.ts +0 -148
- package/src/interfaces/provider.interface.ts +0 -47
- package/src/interfaces/social-auth-provider.interface.ts +0 -131
- package/src/interfaces/storage-adapter.interface.ts +0 -82
- package/src/interfaces/template.interface.ts +0 -510
- package/src/interfaces/token-verifier.interface.ts +0 -110
- package/src/internal.ts +0 -178
- package/src/platform/interfaces.ts +0 -299
- package/src/schemas/auth-config.schema.ts +0 -646
- package/src/services/adaptive-mfa-decision.service.spec.ts +0 -1058
- package/src/services/adaptive-mfa-decision.service.ts +0 -457
- package/src/services/auth-audit.service.spec.ts +0 -675
- package/src/services/auth-audit.service.ts +0 -558
- package/src/services/auth-challenge-helper.service.spec.ts +0 -3227
- package/src/services/auth-challenge-helper.service.ts +0 -825
- package/src/services/auth-flow-context-builder.service.ts +0 -520
- package/src/services/auth-flow-rules.ts +0 -202
- package/src/services/auth-flow-state-definitions.ts +0 -190
- package/src/services/auth-flow-state-machine.service.ts +0 -207
- package/src/services/auth-flow-state-machine.types.ts +0 -316
- package/src/services/auth.service.spec.ts +0 -4195
- package/src/services/auth.service.ts +0 -3727
- package/src/services/challenge.service.spec.ts +0 -1363
- package/src/services/challenge.service.ts +0 -696
- package/src/services/client-info.service.spec.ts +0 -572
- package/src/services/client-info.service.ts +0 -374
- package/src/services/csrf.service.ts +0 -54
- package/src/services/email-verification.service.spec.ts +0 -1229
- package/src/services/email-verification.service.ts +0 -578
- package/src/services/geo-location.service.spec.ts +0 -603
- package/src/services/geo-location.service.ts +0 -599
- package/src/services/index.ts +0 -13
- package/src/services/jwt.service.spec.ts +0 -882
- package/src/services/jwt.service.ts +0 -621
- package/src/services/mfa-base.service.spec.ts +0 -246
- package/src/services/mfa-base.service.ts +0 -611
- package/src/services/mfa.service.spec.ts +0 -693
- package/src/services/mfa.service.ts +0 -960
- package/src/services/password.service.spec.ts +0 -166
- package/src/services/password.service.ts +0 -309
- package/src/services/phone-verification.service.spec.ts +0 -1120
- package/src/services/phone-verification.service.ts +0 -751
- package/src/services/risk-detection.service.spec.ts +0 -1292
- package/src/services/risk-detection.service.ts +0 -1012
- package/src/services/risk-scoring.service.spec.ts +0 -204
- package/src/services/risk-scoring.service.ts +0 -131
- package/src/services/session.service.spec.ts +0 -1293
- package/src/services/session.service.ts +0 -803
- package/src/services/social-account.service.spec.ts +0 -725
- package/src/services/social-auth-base.service.spec.ts +0 -418
- package/src/services/social-auth-base.service.ts +0 -581
- package/src/services/social-auth.service.spec.ts +0 -238
- package/src/services/social-auth.service.ts +0 -436
- package/src/services/social-provider-registry.service.spec.ts +0 -238
- package/src/services/social-provider-registry.service.ts +0 -122
- package/src/services/trusted-device.service.spec.ts +0 -505
- package/src/services/trusted-device.service.ts +0 -339
- package/src/storage/account-lockout-storage.service.spec.ts +0 -310
- package/src/storage/account-lockout-storage.service.ts +0 -89
- package/src/storage/index.ts +0 -3
- package/src/storage/memory-storage.adapter.ts +0 -443
- package/src/storage/rate-limit-storage.service.spec.ts +0 -247
- package/src/storage/rate-limit-storage.service.ts +0 -38
- package/src/templates/html-template.engine.spec.ts +0 -161
- package/src/templates/html-template.engine.ts +0 -688
- package/src/templates/index.ts +0 -7
- package/src/utils/common-passwords.spec.ts +0 -230
- package/src/utils/common-passwords.ts +0 -170
- package/src/utils/context-storage.ts +0 -188
- package/src/utils/cookie-names.util.ts +0 -67
- package/src/utils/cookies.util.ts +0 -94
- package/src/utils/index.ts +0 -12
- package/src/utils/ip-extractor.spec.ts +0 -330
- package/src/utils/ip-extractor.ts +0 -220
- package/src/utils/nauth-logger.spec.ts +0 -388
- package/src/utils/nauth-logger.ts +0 -215
- package/src/utils/pii-redactor.spec.ts +0 -130
- package/src/utils/pii-redactor.ts +0 -288
- package/src/utils/setup/get-repositories.ts +0 -140
- package/src/utils/setup/init-services.ts +0 -422
- package/src/utils/setup/init-social.ts +0 -189
- package/src/utils/setup/init-storage.ts +0 -94
- package/src/utils/setup/register-mfa.ts +0 -165
- package/src/utils/setup/run-nauth-migrations.ts +0 -61
- package/src/utils/token-delivery-policy.ts +0 -38
- package/src/validators/template.validator.ts +0 -219
- package/tsconfig.json +0 -37
- 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
|
-
}
|