Skip to main content

MFAService

Package: @nauth-toolkit/core Type: Service

Central registry service for managing MFA (Multi-Factor Authentication) providers and coordinating MFA operations.

import { MFAService } from '@nauth-toolkit/nestjs';

Overview

The MFAService acts as a registry and orchestrator for MFA provider services (TOTP, SMS, Email, Passkey). It routes MFA operations to the appropriate provider and manages user MFA devices.

note

Auto-injected by framework. No manual instantiation required.

Methods

adminGetMfaStatus()

Get comprehensive MFA status for a target user (admin operation).

async adminGetMfaStatus(dto: AdminGetMFAStatusDTO): Promise<GetMFAStatusResponseDTO>

Parameters

Returns

Errors

CodeWhenDetails
VALIDATION_FAILEDDTO validation fails{ validationErrors: Record<string, string[]> }
NOT_FOUNDUser not foundundefined

Throws NAuthException with the codes listed above.

Example (NestJS)

@Post('admin/mfa/status')
async adminGetStatus(@Body() dto: AdminGetMFAStatusDTO) {
return await this.mfaService.adminGetMfaStatus(dto);
}

adminGetUserDevices()

Get all active MFA devices for a specific user (admin operation).

async adminGetUserDevices(dto: AdminGetUserDevicesDTO): Promise<GetUserDevicesResponseDTO>

Parameters

Returns

Errors

CodeWhenDetails
VALIDATION_FAILEDDTO validation fails{ validationErrors: Record<string, string[]> }
USER_NOT_FOUNDUser not found{ sub: string }

Throws NAuthException with the codes listed above.

Example

@Get('admin/users/:sub/mfa/devices')
@UseGuards(AdminAuthGuard)
async adminGetUserDevices(@Param() dto: AdminGetUserDevicesDTO): Promise<GetUserDevicesResponseDTO> {
return await this.mfaService.adminGetUserDevices(dto);
}

adminRemoveDevice()

Remove a single MFA device by device ID (admin operation). Does not require user context.

async adminRemoveDevice(dto: AdminRemoveDeviceDTO): Promise<RemoveDeviceResponseDTO>

Parameters

Returns

Errors

CodeWhenDetails
NOT_FOUNDDevice not found{ deviceId: number }

Example (NestJS)

@Delete('admin/mfa/devices/:deviceId')
@UseGuards(AdminAuthGuard)
async adminRemoveDevice(@Param() dto: AdminRemoveDeviceDTO): Promise<RemoveDeviceResponseDTO> {
return await this.mfaService.adminRemoveDevice(dto);
}

adminSetPreferredDevice()

Set a specific device as preferred for a user (admin operation).

async adminSetPreferredDevice(dto: AdminSetPreferredDeviceDTO): Promise<AdminSetPreferredDeviceResponseDTO>

Parameters

Returns

  • AdminSetPreferredDeviceResponseDTO - { message: string }

Errors

CodeWhenDetails
NOT_FOUNDUser or device not found{ sub?: string, deviceId?: number }

Example (NestJS)

@Post('admin/users/:sub/mfa/devices/:deviceId/preferred')
@UseGuards(AdminAuthGuard)
async adminSetPreferredDevice(@Param() dto: AdminSetPreferredDeviceDTO): Promise<SetPreferredDeviceResponseDTO> {
return await this.mfaService.adminSetPreferredDevice(dto);
}

getAvailableMethods()

Get available MFA methods for a user. Returns all registered and allowed methods that can be set up.

async getAvailableMethods(dto: GetAvailableMethodsDTO): Promise<GetAvailableMethodsResponseDTO>

Parameters

Returns

Errors

CodeWhenDetails
VALIDATION_FAILEDDTO validation fails{ validationErrors: Record<string, string[]> }
NOT_FOUNDUser not foundundefined

Throws NAuthException with the codes listed above.

VALIDATION_FAILED details

When DTO validation fails, details includes:

{
"validationErrors": {
"sub": ["User sub must be a valid UUID v4 format"]
}
}

Example

@Get('mfa/methods')
async getMethods(@CurrentUser() user: IUser) {
return await this.mfaService.getAvailableMethods({ sub: user.sub });
}

getChallengeData()

Get MFA challenge data during MFA_REQUIRED challenge. Currently only used for passkey authentication to get WebAuthn options. For passkey method, stores challenge in session metadata for verification.

async getChallengeData(dto: GetChallengeDataDTO): Promise<GetChallengeDataResponseDTO>

Parameters

Returns

Errors

CodeWhenDetails
VALIDATION_FAILEDDTO validation fails, challenge not MFA_REQUIRED, no user in session, provider not registered, or method doesn't support challenge data{ validationErrors: Record<string, string[]> } or undefined
CHALLENGE_INVALIDChallenge session not found or invalidundefined
CHALLENGE_EXPIREDChallenge session expiredundefined
CHALLENGE_ALREADY_COMPLETEDChallenge session already completedundefined
CHALLENGE_MAX_ATTEMPTSMaximum challenge attempts exceededundefined
NOT_FOUNDNo MFA device registered for method{ deviceType: 'sms' | 'email' | 'passkey' }

Throws NAuthException with the codes listed above.

VALIDATION_FAILED details

When DTO validation fails, details includes:

{
"validationErrors": {
"session": ["Session token must be a valid UUID v4 format"],
"method": ["Method must be: passkey"]
}
}

When validation fails for other reasons (challenge type, user, provider not registered, method doesn't support challenge data), details is undefined.

NOT_FOUND details

When no device is registered for the method, details includes:

{
"deviceType": "passkey"
}

Example

@Get('mfa/challenge')
async getChallenge(@Query('session') session: string) {
return await this.mfaService.getChallengeData({ session, method: 'passkey' });
}

getMfaStatus()

Get comprehensive MFA status for the current authenticated user including enabled status, configured methods, available methods, backup codes, and exemption information.

async getMfaStatus(): Promise<GetMFAStatusResponseDTO>

Returns

Errors

CodeWhenDetails
FORBIDDENNot authenticated (no user in context)undefined

Throws NAuthException with the codes listed above.

Example

@Get('mfa/status')
async getStatus() {
return await this.mfaService.getMfaStatus();
}

getSetupData()

Get MFA setup data during MFA_SETUP_REQUIRED challenge. Returns provider-specific setup data (QR code for TOTP, options for Passkey, etc.).

async getSetupData(dto: GetSetupDataDTO): Promise<GetSetupDataResponseDTO>

Parameters

Returns

Errors

CodeWhenDetails
VALIDATION_FAILEDDTO validation fails, challenge not MFA_SETUP_REQUIRED, no user in session, or provider not registered{ validationErrors: Record<string, string[]> } or undefined
CHALLENGE_INVALIDChallenge session not foundundefined
CHALLENGE_EXPIREDChallenge session expiredundefined
CHALLENGE_ALREADY_COMPLETEDChallenge session already completedundefined
CHALLENGE_MAX_ATTEMPTSMaximum challenge attempts exceededundefined
PHONE_REQUIREDSMS method requires phone numberundefined

Throws NAuthException with the codes listed above.

VALIDATION_FAILED details

When DTO validation fails, details includes:

{
"validationErrors": {
"session": ["Session token must be a valid UUID v4 format"],
"method": ["Method must be one of: sms, email, totp, passkey"]
}
}

Example

@Get('mfa/setup')
async getSetup(@Query('session') session: string, @Query('method') method: string) {
return await this.mfaService.getSetupData({ session, method: method as MFAMethod });
}

getUserDevices()

Get all active MFA devices for the current authenticated user. User is obtained from the authenticated context.

async getUserDevices(dto?: GetUserDevicesDTO): Promise<GetUserDevicesResponseDTO>

Parameters

Returns

Each device contains:

PropertyTypeDescription
idnumberDevice ID
typeMFADeviceMethodDevice type (totp, sms, email, passkey)
namestringDevice name
isPreferredbooleanWhether this is the preferred device
isActivebooleanWhether the device is active
createdAtDateDevice creation timestamp

Errors

CodeWhenDetails
FORBIDDENNot authenticated (no user in context)undefined

Throws NAuthException with the codes listed above.

Example

@Get('mfa/devices')
async getDevices(): Promise<GetUserDevicesResponseDTO> {
return await this.mfaService.getUserDevices({});
}

hasProvider()

Check if an MFA provider is registered.

hasProvider(dto: HasProviderDTO): HasProviderResponseDTO

Parameters

Returns

Errors

CodeWhenDetails
VALIDATION_FAILEDDTO validation fails{ validationErrors: Record<string, string[]> }

Throws NAuthException with the codes listed above.

VALIDATION_FAILED details

When DTO validation fails, details includes:

{
"validationErrors": {
"methodName": ["Method name must be a string"]
}
}

Example

const result = this.mfaService.hasProvider({ methodName: 'totp' });
if (result.hasProvider) {
// TOTP provider is available
}

listProviders()

Get all registered provider method names.

listProviders(): ListProvidersResponseDTO

Parameters

None.

Returns

Errors

None. This method does not throw errors.

Example

const result = this.mfaService.listProviders();
// Returns: { providers: ['totp', 'sms', 'passkey'] }

removeDevice()

Remove a single MFA device by deviceId (recommended when users can enroll multiple devices for the same method).

async removeDevice(dto: RemoveDeviceDTO): Promise<RemoveDeviceResponseDTO>

Parameters

  • dto - RemoveDeviceDTO

Returns

  • RemoveDeviceResponseDTO - { removedDeviceId: number, removedMethod: string, mfaDisabled: boolean }

Errors

CodeWhen
USER_NOT_FOUNDDevice not found or doesn't belong to authenticated user

Example (NestJS)

@Delete('mfa/devices/:deviceId')
async removeDevice(@Param('deviceId') deviceId: string) {
return await this.mfaService.removeDevice({ deviceId: Number(deviceId) });
}

setMFAExemption()

Grant or revoke a user's exemption from multi-factor authentication requirements. Admin-only operation.

async setMFAExemption(dto: SetMFAExemptionDTO): Promise<SetMFAExemptionResponseDTO>

Parameters

Returns

Errors

CodeWhenDetails
VALIDATION_FAILEDDTO validation fails{ validationErrors: Record<string, string[]> }
NOT_FOUNDUser not foundundefined

Throws NAuthException with the codes listed above.

VALIDATION_FAILED details

When DTO validation fails, details includes:

{
"validationErrors": {
"sub": ["User sub must be a valid UUID v4 format"],
"exempt": ["Exempt must be a boolean"],
"reason": ["Reason must not exceed 500 characters"],
"grantedBy": ["Granted by must not exceed 255 characters"]
}
}

Example

@Post('mfa/exemption')
async setExemption(@Body() dto: SetMFAExemptionDTO) {
return await this.mfaService.setMFAExemption(dto);
}

setPreferredDevice()

Set a specific MFA device as preferred by deviceId. This updates both the device's preferred status and the user's preferred method.

async setPreferredDevice(dto: SetPreferredDeviceDTO): Promise<SetPreferredDeviceResponseDTO>

Parameters

Returns

Example (NestJS)

@Post('mfa/devices/:deviceId/preferred')
async setPreferredDevice(@Param() dto: SetPreferredDeviceDTO) {
return await this.mfaService.setPreferredDevice(dto);
}

setup()

Setup MFA device using appropriate provider. Returns provider-specific setup data that varies by method type.

async setup(dto: SetupMFADTO): Promise<SetupMFAResponseDTO>

Parameters

Returns

Response structure varies by method:

TOTP Response:

{
setupData: {
secret: string; // Base32-encoded TOTP secret
qrCode: string; // QR code as data URL (data:image/png;base64,...)
manualEntryKey: string; // Formatted secret for manual entry (e.g., 'ABCD EFGH IJKL MNOP')
issuer: string; // Issuer name from config
accountName: string; // Account name (typically user's email)
}
}

SMS Response:

// If phone already verified (auto-completed):
{
setupData: {
deviceId: number;
autoCompleted: true;
}
}
// If phone not verified (code sent):
{
setupData: {
maskedPhone: string; // Masked phone number (e.g., '***-***-7890')
}
}

Email Response:

// If email already verified (auto-completed):
{
setupData: {
deviceId: number;
autoCompleted: true;
}
}
// If email not verified (code sent):
{
setupData: {
maskedEmail: string; // Masked email address (e.g., 'u***r@example.com')
}
}

Passkey Response:

{
setupData: {
options: {
challenge: string;
rp: { name: string; id: string };
user: { id: string; name: string; displayName: string };
pubKeyCredParams: Array<{ type: 'public-key'; alg: number }>;
timeout: number;
attestation: 'none' | 'indirect' | 'direct';
authenticatorSelection?: {
authenticatorAttachment?: 'platform' | 'cross-platform';
requireResidentKey?: boolean;
userVerification?: 'required' | 'preferred' | 'discouraged';
};
excludeCredentials?: Array<{ id: string; type: 'public-key'; transports?: string[] }>;
};
}
}

Setup Data by Method:

  • TOTP: No setupData required (or empty object {})
  • SMS: { phoneNumber: string, deviceName?: string } - Phone number in E.164 format (e.g., '+1234567890')
  • Email: { email: string, deviceName?: string } - Email address
  • Passkey: No setupData required (or empty object {})

Errors

CodeWhenDetails
VALIDATION_FAILEDDTO validation fails, provider not registered, method not enabled, service unavailable, or email required{ validationErrors: Record<string, string[]> } or undefined
NOT_FOUNDUser not foundundefined
PHONE_REQUIREDSMS method requires phone numberundefined

Throws NAuthException with the codes listed above.

VALIDATION_FAILED details

When DTO validation fails, details includes:

{
"validationErrors": {
"sub": ["User sub must be a valid UUID v4 format"],
"methodName": ["Method name must be one of: totp, sms, email, passkey"]
}
}

When provider not registered, method not enabled, service unavailable, or email required, details is undefined.

Example - TOTP Setup

@Post('mfa/setup/totp')
async setupTOTP(@CurrentUser() user: IUser) {
const result = await this.mfaService.setup({
methodName: 'totp',
// setupData not required for TOTP
});
// result.setupData contains: { secret, qrCode, manualEntryKey, issuer, accountName }
return result;
}

Example - SMS Setup

@Post('mfa/setup/sms')
async setupSMS(@CurrentUser() user: IUser, @Body() body: { phoneNumber: string; deviceName?: string }) {
const result = await this.mfaService.setup({
methodName: 'sms',
setupData: {
phoneNumber: body.phoneNumber, // E.164 format: '+1234567890'
deviceName: body.deviceName, // Optional: 'My iPhone'
},
});
// If phone verified: result.setupData = { deviceId, autoCompleted: true }
// If phone not verified: result.setupData = { maskedPhone: '***-***-7890' }
return result;
}

Example - Email Setup

@Post('mfa/setup/email')
async setupEmail(@CurrentUser() user: IUser, @Body() body: { email?: string; deviceName?: string }) {
const result = await this.mfaService.setup({
methodName: 'email',
setupData: {
email: body.email || user.email, // Optional if user.email exists
deviceName: body.deviceName, // Optional: 'My Email'
},
});
// If email verified: result.setupData = { deviceId, autoCompleted: true }
// If email not verified: result.setupData = { maskedEmail: 'u***r@example.com' }
return result;
}

Example - Passkey Setup

@Post('mfa/setup/passkey')
async setupPasskey(@CurrentUser() user: IUser) {
const result = await this.mfaService.setup({
methodName: 'passkey',
// setupData not required for Passkey
});
// result.setupData.options contains WebAuthn registration options
// Pass to navigator.credentials.create({ publicKey: result.setupData.options })
return result;
}

verifyCode()

Verify MFA code using appropriate provider. Routes verification to the correct provider based on method name.

async verifyCode(dto: VerifyMFACodeDTO): Promise<VerifyMFACodeResponseDTO>

Parameters

PropertyTypeRequiredDescription
substringYesUser sub (UUID v4)
methodNamestringYesMFA method name. Must be one of: totp, sms, email, passkey, backup
codestring | Record<string, unknown>YesVerification code or credential. For totp/sms/email/backup: string code. For passkey: object with credential and expectedChallenge
deviceIdnumberNoOptional device ID to verify against specific device

Returns

PropertyTypeDescription
validbooleanTrue if verification succeeds

Errors

CodeWhenDetails
VALIDATION_FAILEDDTO validation fails, provider not registered, or backup code verification unavailable{ validationErrors: Record<string, string[]> } or undefined
NOT_FOUNDUser not foundundefined
VERIFICATION_CODE_INVALIDInvalid verification code (SMS/Email methods){ attemptsRemaining: number } or undefined
VERIFICATION_CODE_EXPIREDVerification code has expired (SMS/Email methods)undefined
VERIFICATION_TOO_MANY_ATTEMPTSToo many verification attempts (SMS/Email methods){ maxAttempts: number, currentAttempts: number } or undefined

Example

@Post('mfa/verify')
async verify(@CurrentUser() user: IUser, @Body() body: { method: string; code: string }) {
return await this.mfaService.verifyCode({
sub: user.sub,
methodName: body.method,
code: body.code
});
}

Error Handling