Implementing 2FA in NestJS: A Step-by-Step Guide for Better Security

Implementing 2FA in NestJS: A Step-by-Step Guide for Better Security

Two-factor authentication (2FA) is an important security feature that provides an extra layer of security to user accounts by demanding two forms of identity before giving access. Applications may dramatically improve their security posture and safeguard against illegal access by adding 2FA.

NestJS is a powerful Node.js framework, while Passport is a flexible authentication middleware. Enabling 2FA in a NestJS application requires integrating Google Authenticator, which uses a Time-Based One-Time Password (TOTP) to generate temporary codes.

The process involves configuring modules, implementing endpoints, and protecting sensitive routes. QR codes are also explored for easy setup and integration.

By following this guide, users can enhance their security posture and protect against unauthorized access.

Install necessary packages:

First, you must install the NestJS, Passport, and Google Authenticator packages.

npm install @nestjs/passport passport passport-local passport-jwt passport-google-authenticator

Configure Passport and NestJS modules:

Install the required modules in your NestJS application module. This contains the PassportModule as well as any other modules you may require (e.g., UsersModule, AuthService, and so on).

const { Module } = require('@nestjs/common');
const { PassportModule } = require('@nestjs/passport');
const { JwtModule } = require('@nestjs/jwt');
const { UsersModule } = require('./users/users.module');
const { AuthService } = require('./auth/auth.service');
const { JwtStrategy } = require('./auth/jwt.strategy');

@Module({
  imports: [
    PassportModule,
    UsersModule,
    JwtModule.register({
      secret: 'your_secret_key', 
      signOptions: { expiresIn: '1h' }, // JWT token expiration time
    }),
  ],
  providers: [AuthService, JwtStrategy],
})
class AppModule {}

module.exports = AppModule;
const { Injectable, UnauthorizedException } = require('@nestjs/common');
const { PassportStrategy } = require('@nestjs/passport');
const { Strategy, ExtractJwt } = require('passport-jwt');
const { AuthService } = require('./auth.service');
const { JwtPayload } = require('./interfaces/jwt-payload.interface');

@Injectable()
class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(authService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: 'your_secret_key', // Replace this with your own secret key (same as used in AppModule)
    });
    this.authService = authService;
  }

  async validate(payload) {
    const user = await this.authService.validateUser(payload);
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}

module.exports = JwtStrategy;

Create a 2FA Service

Create TwoFactorAuthenticationService with methods for generating and validating 2FA secrets.

const { Injectable } = require('@nestjs/common');
const { JwtService } = require('@nestjs/jwt');
const { UsersService } = require('../users/users.service');

@Injectable()
class AuthService {
  constructor(usersService, jwtService) {
    this.usersService = usersService;
    this.jwtService = jwtService;
  }

  async validateUser(payload) {

    const user = await this.usersService.findById(payload.sub);
    return user;
  }

  async generateJwtToken(user) {
    // Generate a JWT token based on the user's data.
    const payload = { sub: user.id, username: user.username };
    return this.jwtService.sign(payload);
  }
}

module.exports = AuthService;

Create a 2FA Controller

Create a 2FA-related controller for endpoint interaction.

const { Injectable } = require('@nestjs/common');
const speakeasy = require('speakeasy');

@Injectable()
class TwoFactorAuthenticationService {

  generateTwoFactorAuthenticationSecret(username) {
    const secret = speakeasy.generateSecret({
      name: `ByteScrum Custom App:${username}`,
    });
    return secret.base32;
  }

  generateTwoFactorAuthenticationToken(secret) {
    return speakeasy.totp({
      secret,
      encoding: 'base32',
    });
  }
  validateTwoFactorAuthenticationToken(token, secret) {
    return speakeasy.totp.verify({
      secret,
      encoding: 'base32',
      token,
      window: 1, 
  }
}

module.exports = TwoFactorAuthenticationService;

Generate 2FA Secret and QR Code

Create a TwoFactorAuthenticationController endpoint for 2FA secret generation and QR code URL, enabling user authentication using the Google Authenticator app.

const { Controller, Get, Req, Res } = require('@nestjs/common');
const { TwoFactorAuthenticationService } = require('./two-factor-authentication.service');
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');

@Controller('2fa')
class TwoFactorAuthenticationController {
  constructor(twoFactorAuthenticationService) {
    this.twoFactorAuthenticationService = twoFactorAuthenticationService;
  }

  @Get('generate')
  async generateTwoFactorAuth(req, res) {

    const user = req.user;

    if (!user) {
      return res.status(401).json({ message: 'User not authenticated' });
    }


    if (user.isTwoFactorAuthenticationEnabled) {
      return res.status(400).json({ message: '2FA already enabled!' });
    }


    const secret = this.twoFactorAuthenticationService.generateTwoFactorAuthenticationSecret(user.username);


    const otpAuthUrl = speakeasy.otpauthURL({
      secret: secret,
      label: `YourApp:${user.username}`,
      issuer: 'YourApp', 
    });

    try {

      const qrCodeDataURL = await QRCode.toDataURL(otpAuthUrl);


      return res.status(200).json({ qrCode: qrCodeDataURL });
    } catch (error) {

      console.error('Error generating QR code:', error);
      return res.status(500).json({ message: 'Error generating QR code' });
    }
  }
}

module.exports = TwoFactorAuthenticationController;

Enable 2FA for the User

Create TwoFactorAuthenticationController endpoint to enable 2FA for authenticated users, verifying Google Authenticator code.

const { Controller, Post, Body, Req, Res } = require('@nestjs/common');
const { TwoFactorAuthenticationService } = require('./two-factor-authentication.service');
const speakeasy = require('speakeasy');

@Controller('2fa')
class TwoFactorAuthenticationController {
  constructor(twoFactorAuthenticationService) {
    this.twoFactorAuthenticationService = twoFactorAuthenticationService;
  }

  @Post('enable')
  async enableTwoFactorAuth(@Req() req, @Body() body, @Res() res) {

    const user = req.user;

    if (!user) {
      return res.status(401).json({ message: 'User not authenticated' });
    }


    if (user.isTwoFactorAuthenticationEnabled) {
      return res.status(400).json({ message: '2FA already enabled!' });
    }

    const { token } = body;

    const isValidToken = this.twoFactorAuthenticationService.validateTwoFactorAuthenticationToken(
      token,
      user.twoFactorAuthenticationSecret
    );

    if (!isValidToken) {
      return res.status(401).json({ message: 'Invalid 2FA token' });
    }


    return res.status(200).json({ message: '2FA enabled successfully' });
  }
}

module.exports = TwoFactorAuthenticationController;

Protect Sensitive Routes with 2FA

Custom Passport strategy protects sensitive routes, checks 2FA enabled, prompts 2FA code, and generates code.

const { Injectable, UnauthorizedException } = require('@nestjs/common');
const { PassportStrategy } = require('@nestjs/passport');
const { Strategy } = require('passport-custom');
const speakeasy = require('speakeasy');

@Injectable()
class GoogleAuthenticatorStrategy extends PassportStrategy(Strategy, 'google-authenticator') {
  constructor() {
    super();
  }

  async validate(req) {

    const user = req.user;

    if (!user) {
      throw new UnauthorizedException('User not authenticated');
    }


    if (!user.isTwoFactorAuthenticationEnabled) {
      throw new UnauthorizedException('2FA not enabled');
    }


    const token = req.body.token; 


    const isValidToken = this.validateTwoFactorToken(token, user.twoFactorAuthenticationSecret);

    if (!isValidToken) {
      throw new UnauthorizedException('Invalid 2FA token');
    }

    return user;
  }

  validateTwoFactorToken(token, secret) {

    return speakeasy.totp.verify({
      secret,
      encoding: 'base32',
      token,
      window: 1, 
    });
  }
}

module.exports = GoogleAuthenticatorStrategy;

Validate 2FA Code for Protected Routes

Implement a custom Passport strategy verification process for 2FA code validation.

const { Injectable, UnauthorizedException } = require('@nestjs/common');
const { PassportStrategy } = require('@nestjs/passport');
const { Strategy } = require('passport-custom');
const speakeasy = require('speakeasy');

@Injectable()
class GoogleAuthenticatorStrategy extends PassportStrategy(Strategy, 'google-authenticator') {
  constructor() {
    super();
  }

  async validate(req) {

    const user = req.user;

    if (!user) {
      throw new UnauthorizedException('User not authenticated');
    }

    if (!user.isTwoFactorAuthenticationEnabled) {
      throw new UnauthorizedException('2FA not enabled');
    }


    const token = req.body.token;

    const isValidToken = this.validateTwoFactorToken(token, user.twoFactorAuthenticationSecret);

    if (!isValidToken) {
      throw new UnauthorizedException('Invalid 2FA token');
    }

    return user;
  }

  validateTwoFactorToken(token, secret) {

    return speakeasy.totp.verify({
      secret,
      encoding: 'base32',
      token,
      window: 1, 
    });
  }
}

module.exports = GoogleAuthenticatorStrategy;
💡
Install necessary packages for NestJS, Passport, and Google Authenticator. Configure Passport and NestJS modules, create a 2FA service, create a 2FA controller, generate 2FA secret and QR code, enable 2FA for users, protect sensitive routes with a custom Passport strategy, validate 2FA code for protected routes, and thoroughly test the 2FA flow for security.
Summary
Implementing Two-Factor Authentication (2FA) in your NestJS application successfully and securely necessitates careful consideration of extra security considerations and best practices. While the offered quick overview provides a rough idea of the required procedures, it is critical to consult the official NestJS documentation and community resources for further implementation instructions.