import { IUser } from '../interfaces/entities.interface'; import { StorageAdapter } from '../interfaces/storage-adapter.interface'; import { InternalAuthAuditService as AuthAuditService } from './auth-audit.service'; import { RiskFactor } from '../enums/risk-factor.enum'; import { RiskDetectionService } from './risk-detection.service'; import { RiskScoringService } from './risk-scoring.service'; import { ClientInfoService } from './client-info.service'; import { NAuthConfig, AdaptiveMFARiskEventPayload } from '../interfaces/config.interface'; import { NAuthLogger } from '../utils/nauth-logger'; import { HookRegistryService } from './hook-registry.service'; /** * Adaptive MFA decision result */ export interface AdaptiveMFADecision { /** * Action to take */ action: 'allow' | 'require_mfa' | 'block_signin'; /** * Risk score (0-100) */ riskScore: number; /** * Risk level classification */ riskLevel: 'low' | 'medium' | 'high'; /** * Detected risk factors * Array of RiskFactor enum values (stored as strings at runtime) */ riskFactors: RiskFactor[]; /** * Whether user should be notified */ notifyUser: boolean; /** * Whether lifecycle hook overrode the decision */ hookOverride: boolean; /** * Risk event payload (included when action requires it or notifyUser is true) * Contains full client context for use in blockUserSignIn or audit logging */ payload?: AdaptiveMFARiskEventPayload; } /** * Adaptive MFA Decision Service * * Makes context-aware MFA requirement decisions based on risk analysis. * Supports multiple actions (allow, require_mfa, block_signin) based on risk level. * * **Decision Flow:** * 1. Detect risk factors (via RiskDetectionService) * 2. Calculate risk score (via RiskScoringService) * 3. Determine risk level and action from configuration * 4. Call lifecycle hooks if notifyUser is true * 5. Record audit event (non-blocking) * 6. Return decision object * * **Default Risk Levels:** * - Low (0-20): action 'allow', notifyUser false * - Medium (21-50): action 'require_mfa', notifyUser true * - High (51-100): action 'require_mfa', notifyUser true (conservative default) * * **User Blocking:** * When action is 'block_signin', user is blocked in storage adapter with optional TTL. * Block status is checked before evaluation to prevent blocked users from attempting sign-in. * * @example * ```typescript * const decision = await adaptiveMFADecisionService.evaluateAdaptiveMFA(user, 'password'); * if (decision.action === 'block_signin') { * throw new NAuthException(AuthErrorCode.SIGNIN_BLOCKED_HIGH_RISK, 'Sign-in blocked'); * } * return decision.action === 'require_mfa'; * ``` */ export declare class AdaptiveMFADecisionService { private readonly riskDetectionService; private readonly riskScoringService; private readonly storageAdapter; private readonly clientInfoService; private readonly config; private readonly logger; private readonly auditService?; private readonly hookRegistry?; /** * Default risk level configuration * * Conservative defaults that prioritize security: * - Low risk: Allow without MFA (normal flow) * - Medium risk: Require MFA * - High risk: Require MFA (conservative - don't block by default) */ private readonly defaultRiskLevels; constructor(riskDetectionService: RiskDetectionService, riskScoringService: RiskScoringService, storageAdapter: StorageAdapter, clientInfoService: ClientInfoService, config: NAuthConfig, logger: NAuthLogger, auditService?: AuthAuditService | undefined, // Optional - audit trail service (enabled via config.auditLogs.enabled) hookRegistry?: HookRegistryService | undefined); /** * Resolve the configured block scope for adaptive MFA sign-in blocking. * * @returns Block scope (defaults to `user`) * @private */ private getBlockedSignInScope; /** * Build the storage key for a sign-in block. * * Always keyed on the user alone. The scope discriminator lives *inside* the record * (see `blockRecordApplies`) rather than in the key, because the key decides whether * the block can be found at all: derived from `deviceToken` or `ipAddress` — values * the client supplies — an attacker evaded a stored block simply by dropping the * `x-device-token` header, which sent the read to a different key. * * @param userId - Internal user ID * @returns Storage key * @private */ private buildBlockKey; /** * Hash a device token for storage. * * The plaintext token is a bearer credential that skips MFA, so only its digest is * ever persisted — matching how `TrustedDeviceService` stores it. * * @param deviceToken - Plaintext device token from the client * @returns Hex-encoded SHA-256 digest * @private */ private hashDeviceToken; /** * Whether a stored block applies to the request being evaluated. * * `scope` narrows a block to one device or IP to avoid locking a user out of every * device (see `blockedSignIn.scope`). The discriminator is client-supplied, so the * comparison fails **closed**: a request that presents no device token or no IP * cannot claim to be a different device, and stays blocked. A request presenting a * genuinely different one is not blocked by this record — it is re-scored from * scratch, and the same risk factors block it again. * * @param record - The parsed block record * @param clientInfo - Current client context * @returns True when the block covers this request * @private */ private blockRecordApplies; /** * Evaluate adaptive MFA requirement with risk-based actions * * Main entry point for adaptive MFA evaluation. Analyzes current login context, * calculates risk score, determines action, and calls lifecycle hooks. * * @param user - User being authenticated * @param authMethod - Authentication method ('password', 'google', 'apple', etc.) * @returns Decision object with action, risk details, and hook override status * * @example * ```typescript * const decision = await adaptiveMFADecisionService.evaluateAdaptiveMFA(user, 'password'); * if (decision.action === 'block_signin') { * // Handle blocking * } * ``` */ evaluateAdaptiveMFA(user: IUser, authMethod: string): Promise; /** * Determine risk level and action based on score and configured thresholds * * Evaluates risk score against configured thresholds in order: low → medium → high. * Returns the first level that the score falls within. * * @param riskScore - Calculated risk score (0-100) * @param riskLevels - Configured risk level thresholds * @returns Risk level, action, and notifyUser flag * @private */ private determineRiskLevelAndAction; /** * Check if user is currently blocked due to high-risk sign-in * * Reads the single block record at `adaptive_mfa_block:{userId}`, then applies the * record's own `scope` to decide whether it covers this request. Keying on the user * alone is deliberate: a key built from the request's device token or IP could be * moved by the caller, so dropping a header made a stored block unfindable. * * @param userId - Internal user ID (integer) * @returns Block status with expiration and message if blocked * * @example * ```typescript * const blockStatus = await adaptiveMFADecisionService.isUserBlocked(user.id); * if (blockStatus.blocked) { * throw new NAuthException(AuthErrorCode.SIGNIN_BLOCKED_HIGH_RISK, blockStatus.message); * } * ``` */ isUserBlocked(userId: number): Promise<{ blocked: boolean; expiresAt?: Date; message?: string; }>; /** * Block user sign-in due to high risk * * Stores block in storage adapter with optional TTL. Block data includes: * - userId, userSub (for reference) * - message (shown to user) * - riskScore, riskFactors (for audit) * - blockedAt, expiresAt (timestamps) * - scope, and the hashed device token or IP it applies to * * One record per user: a later block for the same user replaces the earlier one. * * Also calls onSignInBlocked lifecycle hook if configured. * * @param user - User to block * @param payload - Risk event payload with all context * * @example * ```typescript * await adaptiveMFADecisionService.blockUserSignIn(user, payload); * ``` */ blockUserSignIn(user: IUser, payload: AdaptiveMFARiskEventPayload): Promise; /** * Clear user block (manual unblock) * * Removes the block from storage adapter, allowing user to sign in again. * Useful for admin actions or when risk situation has improved. * * @param userId - Internal user ID (integer) * * @example * ```typescript * await adaptiveMFADecisionService.clearUserBlock(user.id); * ``` */ clearUserBlock(userId: number): Promise; } //# sourceMappingURL=adaptive-mfa-decision.service.d.ts.map