import { Repository } from 'typeorm'; import { BaseMFADevice, BaseUser } from '../entities'; import { IMFAProviderService } from '../interfaces/mfa-provider.interface'; import { ChallengeService } from './challenge.service'; import { NAuthConfig } from '../interfaces/config.interface'; import { NAuthLogger } from '../utils/nauth-logger'; import { InternalAuthAuditService as AuthAuditService } from './auth-audit.service'; import { ClientInfoService } from './client-info.service'; import { HookRegistryService } from './hook-registry.service'; import { GetAvailableMethodsDTO, GetAvailableMethodsResponseDTO, GetChallengeDataDTO, GetChallengeDataResponseDTO, AdminGetMFAStatusDTO, GetMFAStatusResponseDTO, GetSetupDataDTO, GetSetupDataResponseDTO, AdminGetUserDevicesDTO, GetUserDevicesDTO, GetUserDevicesResponseDTO, HasProviderDTO, HasProviderResponseDTO, ListProvidersResponseDTO, AdminRemoveDeviceDTO, AdminSetPreferredDeviceDTO, AdminSetPreferredDeviceResponseDTO, RemoveDeviceDTO, RemoveDeviceResponseDTO, SetMFAExemptionDTO, SetMFAExemptionResponseDTO, SetPreferredDeviceDTO, SetPreferredDeviceResponseDTO, SetupMFADTO, SetupMFAResponseDTO, VerifyMFACodeDTO, VerifyMFACodeResponseDTO } from '../dto'; /** * MFA Service Registry * * Central registry for managing MFA provider services. * Routes requests to the appropriate provider based on method name. * * Provider services (TOTP, SMS, Passkey) automatically register themselves * when their modules are imported via OnModuleInit. * * **Key Features:** * - Provider registration and lookup * - Unified interface for MFA operations * - Routing verification requests to correct provider * - Device management operations * * @example * ```typescript * @Controller('auth') * export class AuthController { * constructor(private readonly mfaService: MFAService) {} * * @Post('mfa/verify') * async verifyMFA(@Body() dto: { method: string; code: string }) { * const provider = this.mfaService.getProvider(dto.method); * return await provider.verify(dto.code); * } * } * ``` */ export declare class MFAService { private readonly mfaDeviceRepository; private readonly userRepository; private readonly challengeService?; private readonly config?; private readonly logger?; private readonly auditService?; private readonly clientInfoService?; private readonly hookRegistry?; private readonly providers; /** * Shared implementation for retrieving MFA status by target user sub. * * @param sub - Target user sub (UUID v4) * @returns Comprehensive MFA status */ private getMfaStatusBySub; /** * Fetch active MFA devices for a given internal user ID. * * @param userId - Internal DB user ID * @returns Active MFA devices */ private getActiveDevicesForUserId; /** * Resolve a target user by `sub` (admin-style targeting). * * @param sub - Target user sub (UUID v4) * @returns User entity * @throws {NAuthException} NOT_FOUND when user is not found */ private getUserBySubOrThrow; /** * Shared implementation for removing MFA devices. * * @param targetUser - Target user (self-service or admin target) * @param methodType - MFA method to remove (normalized) * @param removedBy - Actor performing the removal */ /** * Shared implementation for removing a single MFA device by device ID. * * @param targetUser - Target user (self-service or admin target) * @param deviceId - MFA device ID to remove * @param removedBy - Actor performing the removal * @returns Removal result * @throws {NAuthException} NOT_FOUND when device is not found for the user */ private removeDeviceInternal; /** * Resolve a user entity by flexible identifier. * * WHY: Admin APIs typically accept a generic identifier (email/username/phone/sub) for consistency. * MFA exemption is admin-only, so we support the same ergonomics. * * @param identifier - User identifier (email/username/phone/sub) * @returns User entity, or null when not found */ private findUserByIdentifier; constructor(mfaDeviceRepository: Repository, userRepository: Repository, challengeService?: ChallengeService | undefined, config?: NAuthConfig | undefined, logger?: NAuthLogger | undefined, auditService?: AuthAuditService | undefined, clientInfoService?: ClientInfoService | undefined, hookRegistry?: HookRegistryService | undefined); /** * Get current user from authenticated context * * @returns Current authenticated user * @throws {NAuthException} If user not found in context */ private getCurrentUserOrThrow; /** * Execute a callback with a specific user bound into CURRENT_USER context. * * This is required for flows where the user is resolved outside of request auth context * (e.g., challenge sessions) but providers must still derive the user from context. * * @param user - User to bind into context * @param callback - Callback to execute * @returns Callback result */ private withUserContext; /** * Register an MFA provider * * Called automatically by provider modules during initialization. * Provider method names must be unique. * * @param provider - Provider service instance (must have methodName property) * @throws {NAuthException} If provider is already registered * * @example * ```typescript * // In provider module's OnModuleInit * this.mfaService.registerProvider(this.totpProvider); * ``` */ registerProvider(provider: IMFAProviderService): void; /** * Get a provider by method name * * @param methodName - Method name (e.g., 'totp', 'sms', 'passkey') * @returns Provider service instance * @throws {NAuthException} If provider is not registered * * @example * ```typescript * const totpProvider = this.mfaService.getProvider('totp'); * const setupData = await totpProvider.setup(user); * ``` */ getProvider(methodName: string): IMFAProviderService; /** * Check if a provider is registered * * @param dto - Request DTO with method name * @returns Response DTO with hasProvider flag * * @example * ```typescript * const result = await this.mfaService.hasProvider({ methodName: 'totp' }); * if (result.hasProvider) { * // TOTP is available * } * ``` */ hasProvider(dto: HasProviderDTO): HasProviderResponseDTO; /** * Get all registered provider method names * * @returns Response DTO with array of method names * * @example * ```typescript * const result = this.mfaService.listProviders(); // { providers: ['totp', 'sms', 'passkey'] } * ``` */ listProviders(): ListProvidersResponseDTO; /** * Get available MFA methods for a user * * Returns list of methods that are: * - Registered as providers * - Allowed by configuration * * This returns ALL methods that can be set up, not just ones the user has configured. * Use getUserDevices() to check which methods the user has actually set up. * * @param dto - Request DTO with user sub * @returns Response DTO with array of available method names * * @example * ```typescript * const result = await this.mfaService.getAvailableMethods({ sub: user.sub }); * // Returns: { availableMethods: ['totp', 'sms', 'passkey'] } * ``` */ getAvailableMethods(dto: GetAvailableMethodsDTO): Promise; /** * Verify MFA code using appropriate provider * * Routes the verification request to the correct provider based on method name. * * @param dto - Request DTO with user sub, method name, code, and optional device ID * @returns Response DTO with verification result * @throws {NAuthException} If method is not available or verification fails * * @example * ```typescript * // Verify TOTP code * const result = await this.mfaService.verifyCode({ * sub: user.sub, * methodName: 'totp', * code: '123456' * }); * * // Verify backup code * const result = await this.mfaService.verifyCode({ * sub: user.sub, * methodName: 'backup', * code: 'ABC12345' * }); * ``` */ verifyCode(dto: VerifyMFACodeDTO): Promise; /** * Setup MFA device using appropriate provider * * @param dto - Request DTO with user sub, method name, and optional setup data * @returns Response DTO with provider-specific setup data * * @example * ```typescript * const result = await this.mfaService.setup({ * sub: user.sub, * methodName: 'totp' * }); * // Returns: { setupData: { secret, qrCode, manualEntryKey } } * ``` */ setup(dto: SetupMFADTO): Promise; /** * Get user's MFA devices * * User self-service method: current user is derived from authenticated context. * * @param _dto - Optional (empty) DTO for validation consistency * @returns Response DTO with array of MFA devices * * @example * ```typescript * const result = await this.mfaService.getUserDevices(); * // Returns: { devices: [...] } * ``` */ getUserDevices(_dto?: GetUserDevicesDTO): Promise; /** * Get comprehensive MFA status for the current authenticated user (self-service). * * @returns Response DTO with complete MFA status */ getMfaStatus(): Promise; /** * Get comprehensive MFA status for a target user (admin-only). * * @param dto - Admin request DTO with target user sub * @returns Response DTO with complete MFA status */ adminGetMfaStatus(dto: AdminGetMFAStatusDTO): Promise; /** * Get MFA devices for a specific user (admin-only). * * Admin operation that retrieves all active MFA devices for a target user. * Returns device details including id, name, type, and isPreferred status. * * @param dto - Admin request DTO with target user sub * @returns Response DTO with array of MFA devices * @throws {NAuthException} If user not found * * @example * ```typescript * const result = await mfaService.adminGetUserDevices({ sub: 'user-uuid' }); * // Returns: { devices: [{ id: 1, name: 'My Authenticator', type: 'totp', isPreferred: true, ... }] } * ``` */ adminGetUserDevices(dto: AdminGetUserDevicesDTO): Promise; /** * Remove MFA devices by method type * * Comprehensive method that handles all aspects of MFA device removal: * - Uses the authenticated user context (self-service) * - Validates method type * - Removes all active devices of the specified method type * - Updates user's preferred method if the removed method was preferred * - Updates device primary flags * - Disables MFA if this was the last device * - Creates MFA_SETUP_REQUIRED challenge if MFA enforcement requires it * * This method encapsulates all database operations related to MFA device removal, * ensuring the consumer app doesn't need to directly manipulate nauth_* tables. * * @param dto - Request DTO with method type * @returns Response DTO with deletedCount and whether MFA was disabled * @throws {NAuthException} If user not found, invalid method type, or no devices found * * @example * ```typescript * // Consumer app controller * @Delete('mfa/devices/:method') * async removeMFAMethod(@CurrentUser() user: IUser, @Param('method') method: string) { * const result = await this.mfaService.removeDevices({ methodType: method }); * return { message: 'MFA method removed successfully', ...result }; * } * ``` */ /** * Remove a single MFA device by device ID (self-service). * * WHY: Users can register multiple devices per method (e.g., multiple passkeys, multiple TOTP apps), * so deleting by method alone is often too destructive. * * @param dto - Request DTO with deviceId * @returns Removal response (removed device id/method and whether MFA was disabled) * @throws {NAuthException} NOT_FOUND when device does not exist or does not belong to the user * * @example * ```typescript * const result = await mfaService.removeDevice({ deviceId: 123 }); * ``` */ removeDevice(dto: RemoveDeviceDTO): Promise; /** * Admin: Remove a single MFA device by device ID. * * Admin APIs are allowed to target any user's device. The owning user is resolved * from the device record and the same internal removal logic is reused. * * @param dto - Admin request DTO with deviceId * @returns Removal response * @throws {NAuthException} NOT_FOUND when device or owning user is not found * * @example * ```typescript * const result = await mfaService.adminRemoveDevice({ deviceId: 123 }); * ``` */ adminRemoveDevice(dto: AdminRemoveDeviceDTO): Promise; /** * Admin: Set a user's preferred MFA device. * * Updates the preferred device for a specified user by device ID. * This is an admin-only operation that allows administrators to manage * user MFA preferences. * * @param dto - DTO containing user sub and device ID * @returns Success message * @throws {NAuthException} NOT_FOUND | VALIDATION_FAILED * * @example * ```typescript * const result = await mfaService.adminSetPreferredDevice({ * sub: 'user-uuid', * deviceId: 123 * }); * // Returns: { message: "Preferred MFA device updated" } * ``` */ adminSetPreferredDevice(dto: AdminSetPreferredDeviceDTO): Promise; /** * Set a specific MFA device as the user's preferred device (self-service). * * This updates: * - The user's preferred MFA method (set to the device's method) * - Device `isPrimary` flags (exactly one active device becomes primary/preferred) * * @param dto - Request DTO with deviceId * @returns Success message * @throws {NAuthException} NOT_FOUND when device does not exist or does not belong to the user * * @example * ```typescript * await mfaService.setPreferredDevice({ deviceId: 123 }); * ``` */ setPreferredDevice(dto: SetPreferredDeviceDTO, updatedBy?: 'user' | 'admin'): Promise; /** * Grant or revoke a user's exemption from multi-factor authentication (MFA) requirements. * * SECURITY: This admin-only operation updates the user's MFA exemption status, logs the action, * and records an audit event. MFA exemption bypasses MFA at login, but all other security controls remain enforced. * * @param dto - Request DTO with sub, exempt flag, reason, and grantedBy * @returns Response DTO with updated exemption fields * @throws {NAuthException} If the user is not found * * @example * ```typescript * // Grant MFA exemption * await mfaService.setMFAExemption({ * sub: 'a21b654c-2746-4168-acee-c175083a65cd', * exempt: true, * reason: 'Business partner requires MFA bypass', * grantedBy: 'admin@example.com' * }); * * // Revoke MFA exemption * await mfaService.setMFAExemption({ * sub: 'a21b654c-2746-4168-acee-c175083a65cd', * exempt: false, * reason: 'MFA now mandatory for this user', * grantedBy: 'admin@example.com' * }); * ``` */ setMFAExemption(dto: SetMFAExemptionDTO): Promise; /** * Get MFA setup data during MFA_SETUP_REQUIRED challenge * * Returns provider-specific setup data: * - TOTP: { secret, qrCode, manualEntryKey } * - SMS: { maskedPhone } or error if phone required * - Passkey: WebAuthn registration options * * @param dto - Request DTO with session token, method, and optional setup data * @returns Response DTO with provider-specific setup data * @throws {NAuthException} INVALID_CHALLENGE_SESSION | VALIDATION_FAILED | PHONE_REQUIRED * * @example * ```typescript * const result = await mfaService.getSetupData({ * session: 'session-token', * method: 'totp' * }); * // Returns: { setupData: { secret: '...', qrCode: '...', manualEntryKey: '...' } } * * const result = await mfaService.getSetupData({ * session: 'session-token', * method: 'sms', * setupData: { phoneNumber: '+1234567890' } * }); * // Returns: { setupData: { maskedPhone: '***-***-7890' } } * ``` */ getSetupData(dto: GetSetupDataDTO): Promise; /** * Get MFA challenge data during MFA_REQUIRED challenge * * Supports multiple MFA methods: * - Passkey: Returns WebAuthn authentication options * - SMS: Sends SMS code and returns masked phone number (string) * - Email: Sends email code and returns masked email address (string) * * Note: SMS codes are automatically sent when challenge is created if SMS is preferred method. * This endpoint allows switching to a different method or requesting a new code. * * Session metadata updates: * - Stores the current method in session metadata * - This allows resendCode to use the correct method when user switches MFA methods * - For passkey, also stores the challenge for verification * * @param dto - Request DTO with session token and method * @returns Response DTO with provider-specific challenge data * @throws {NAuthException} INVALID_CHALLENGE_SESSION | VALIDATION_FAILED * * @example * ```typescript * // Passkey: Get WebAuthn options * const result = await mfaService.getChallengeData({ * session: 'session-token', * method: 'passkey' * }); * // Returns: { challengeData: { challenge: '...', allowCredentials: [...], ... } } * * // SMS: Send code and get masked phone * const result = await mfaService.getChallengeData({ * session: 'session-token', * method: 'sms' * }); * // Returns: { challengeData: '***-***-1234' } * * // Email: Send code and get masked email * const result = await mfaService.getChallengeData({ * session: 'session-token', * method: 'email' * }); * // Returns: { challengeData: 'u***r@example.com' } * ``` */ getChallengeData(dto: GetChallengeDataDTO): Promise; } //# sourceMappingURL=mfa.service.d.ts.map