@enteocode/nestjs-mfa
TypeScript icon, indicating that this package has built-in type declarations

1.0.0 • Public • Published

NestJS 2FA/MFA Module

Build Status License Coverage

RFC‑compliant Multi‑Factor Authentication (2FA/MFA) module for NestJS, built on otplib. Pluggable storage, QR codes, recovery codes, and event‑driven architecture.

Table of Contents

Requirements and Peer Dependencies

Features

  • Context-aware: HTTP, RPC, WebSocket, etc.
  • Services: Registration, validation, QR generation, recovery
  • Security: Encrypted storage
  • Extensible: Pluggable extractors, decorators, and event hooks
  • Optimized: Streamable QR codes, low memory footprint

Installation

npm i -S @enteocode/nestjs-mfa

Module Registration

Configure using forRoot or forRootAsync, according to NestJS standards:

app.module.ts

import { Module } from '@nestjs/common';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { MfaModule } from '../src';
import { createKeyv } from '@keyv/redis';

@Module({
    imports: [
        // Required for events
        EventEmitterModule.forRoot(),
        
        MfaModule.forRoot({
            // Displayed in authenticator apps
            issuer: 'My Application',
            
            // Persistent key-value adapter for KeyV
            store: createKeyv(/* ... */),
            
            // AES-256-GCM
            cipher: 'my-32-byte-encryption-key'
        })
    ]
})
export class AppModule {}

Important: The EventEmitterModule must be registered.

Configuration

Name Type Required Default Description
issuer string Yes App name for authenticator display
store Keyv Yes Persistent key-value store
ttl number No 30 TOTP token lifetime (seconds)
serializer SerializerInterface No V8 Data serialization (JSON, V8, etc.)
cipher string No AES-256 key for encrypted storage

Hint: Provide a cipher to protect user secrets according to NIS2 and GDPR standards

Store

Persistent storage is required.

Supported Keyv adapters: Etcd, Memcache, Mongo, MySQL, Postrgres, Redis, SQLite, Valkey.

Usage

After registration, you can use the MfaService to implement it to your setup by injecting it to your service or controller.

HTTP

mfa.controller.ts

import { Body, Controller, InternalServerErrorException, ParseUUIDPipe, Post, StreamableFile } from '@nestjs/common';
import { Format, MfaService, TokenType } from '@enteocode/nestjs-mfa';
import { TokenVerificationRequest } from './mfa.token-verification.request.ts';

@Controller('mfa')
class MfaController {
    constructor(private readonly mfa: MfaService) {}

    @Post('enable/:user')
    public async enable(@Param('user', ParseUUIDPipe) user: string): Promise<StreamableFile> {
        const secret = await this.mfa.enable(user);

        if (!secret) {
            throw new InternalServerErrorException('Cannot enable MFA for user');
        }
        return this.mfa.generate(user, TokenType.AUTHENTICATOR, Format.PNG);
    }

    @Post('verify')
    public async verify(@Body() { user, token }: TokenVerificationRequest): Promise<boolean> {
        return this.mfa.verify(user, token);
    }
}

Validation DTO

token-verification.request.ts

import { IsUUID } from 'class-validator';
import { IsToken, Token } from '@enteocode/nestjs-mfa';
 
// ValidationPipe based flow
 
export class TokenVerificationRequest {
    @IsUUID()
    user: string;
    
    @IsToken() // Built-in validator
    token: Token;
}

Microservices

mfa.controller.ts

import { Controller } from '@nestjs/common';
import { EventPattern, Payload } from '@nestjs/microservices';

@Controller()
class MfaController {
    constructor(private readonly mfa: MfaService) {}
    
    @MessagePattern({ cmd: 'mfa.enable '})
    public async enable(@Payload() user: string): Promise<string> {
        /* ... */
    }
}

QR Code

The QR code contains a Key URI, originally defined by Google for its Authenticator app and later adopted by other authenticator applications. This URI encodes the secret along with key properties (such as account name and issuer) and is used to register the authenticator with a user’s device.

Conceptually, it's similar to asymmetric cryptography: the secret acts like a private key, and the app uses it to generate public one-time codes.

You can choose to generate the QR code on the client side or the backend side:

  • Client-side generation reduces backend load and cost.
  • Backend-side generation is handled by the module using streamed rendering to keep memory and CPU usage minimal.

QR codes are only required for the initial pairing with the authenticator app.

Example

mfa.authenticator.controller.ts

import { Controller, Get, Param, ParseUUIDPipe, StreamableFile } from '@nestjs/common';
import { MfaService, TokenType } from '@enteocode/nestjs-mfa';

@Controller('mfa/generate')
class MfaAuthenticatorController {
    constructor(private readonly mfa: MfaService) {}

    @Get(':user/qr')
    public generateQrCode(@Param('user', ParseUUIDPipe) user: string): Promise<StreamableFile> {
        return this.mfa.generate(user, TokenType.AUTHENTICATOR, Format.WEBP)
    }

    @Get(':user/uri')
    public generateKeyUri(@Param('user', ParseUUIDPipe) user: string): Promise<string> {
        return this.mfa.generate(user, TokenType.AUTHENTICATOR)
    }
    
    @Get(':user/token')
    public generateToken(@Param('user', ParseUUIDPipe) user: string): Promise<string> {
        // Manual token creation instead of authenticator
        // Useful to generate one-time password for email based second factor
        
        return this.mfa.generate(user, TokenType.TIMEOUT, { step: 5 * 60, digits: 8 })
    }
}

Formats

Format Avg. Size MIME Type
WebP 242B image/webp
PNG 406B image/png
AVIF 674B image/avif
JPEG 3.7KB image/jpeg

Note: Sizes measured with test data, actual results may vary

Advanced Usage

Credential Extractors

To avoid manually verifying tokens across multiple endpoints, you can implement credential extractors. These provide a context-specific mechanism for extracting credentials (such as user ID and token) and feeding them into a parameter decorator. This makes the solution reusable, streamlined, and transport-agnostic (HTTP, RPC, WebSockets, etc.).

Hint: you can create multiple extractors and the first one will be ued which supports the actual ExecutionContext.

Example

An example of a HTTP extractor, that gets credentials from the header, with the pattern:

HTTP Header

X-MFA: otp://user:012345@authenticator

mfa.http.credentials-extractor.ts

import { ExecutionContext, Injectable } from '@nestjs/common';
import { FastifyRequest } from 'fastify';
import { MfaCredentialsExtractor, MfaCredentialsExtractorInterface } from '@enteocode/nestjs-mfa';

@MfaCredentialsExtractor('http')
@Injectable()
class MfaHttpCredentialsExtractor implements MfaCredentialsExtractorInterface {
    public supports(context: ExecutionContext): boolean {
        return Boolean(this.getHeader(context));
    }

    public getUserIdentifier(context: ExecutionContext): Identifier {
        return URL.parse(this.getHeader(context)).username;
    }

    public getToken(context: ExecutionContext): Token {
        return URL.parse(this.getHeader(context)).password;
    }
    
    // Helper
    
    private getHeader(context: ExecutionContext, header: string = 'x-mfa'): string {
        return context.switchToHttp().getRequest<FastifyRequest>().headers[header];
    }
}

Hint: If you leave the MfaCredentialsExtractor decorator's parameter empty, it will be called to check support on every context.

You have to add this extractor to your list of providers to be part of the Dependency Injection container in order to be detectable.
You are ready to use:

mfa.controller.ts

import { Controller, Get } from '@nestjs/common';
import { MfaCredentials, MfaCredentialsInterface } from '@enteocode/nestjs-mfa';

@Controller()
class MfaController {
    @Get()
    public async login(@MfaCredentials({ required: true, validate: true }) credentials: MfaCredentialsInterface) {
        // credentials.user     [Identifier]    The unique identifier of the user (email, UUID, etc.)
        // credentials.token    [Token]         The 6-digit token
    }
}

Hint: All options are optional, credentials will be automatically validated if validate is true

Events

Authentication Events

Event Payload Description
mfa.enabled { user: string, secret: string } MFA activated for user
mfa.disabled { user: string } MFA deactivated
mfa.failed { user: string, token: string } Invalid token provided

Recovery Events

Event Payload Description
mfa.recovery.enabled { user: string, codes: Set<string>} Backup codes generated
mfa.recovery.disabled { user: string } Backup codes deleted
mfa.recovery.used { user: string, code: string } Backup code consumed
mfa.recovery.failed { user: string, code: string } Backup code validation failed

Example

app.event-listener.ts

import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { AuthenticationFailedEvent, EventType } from '@enteocode/nestjs-mfa';

@Injectable()
class AppEventListener {
    @OnEvent(EventType.AUTHENTICATION_FAILED)
    onMfaAuthenticationFailed(event: AuthenticationFailedEvent) {
        // Block user after 5 attempts
    }
}

Compliance

License

MIT © 2025, Ádám Székely

Package Sidebar

Install

npm i @enteocode/nestjs-mfa

Weekly Downloads

49

Version

1.0.0

License

MIT

Unpacked Size

92.6 kB

Total Files

46

Last publish

Collaborators

  • enteocode