Two-Factor Authentication (2FA) in NestJS: A Comprehensive Guide

Introduction

Adding Two-Factor Authentication (2FA) enhances the security of user accounts by mandating a secondary form of verification alongside the password. This guide will demonstrate how to incorporate 2FA into a NestJS application using the @nestjs/passport​ module and the speakeasy​ library for creating and authenticating one-time passwords (OTPs).

Setting Up the NestJS Project

Let's start by setting up a new NestJS project and installing all the required dependencies:

$ nest new nest-2fa-example
$ cd nest-2fa-example
$ npm install @nestjs/passport passport passport-local speakeasy


Creating the User Entity

We'll create a User​ entity with fields for the user's email, password hash, and secret key for 2FA.

// user.entity.ts
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ unique: true })
  email: string;

  @Column()
  password: string;

  @Column({ nullable: true })
  secretKey: string; // Secret key for 2FA
}


Implementing Authentication Strategies

We'll use Passport.js with the @nestjs/passport​ module to implement local and 2FA authentication strategies.

Local Strategy
// local.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { AuthService } from './auth.service';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private authService: AuthService) {
    super();
  }

  async validate(email: string, password: string): Promise<any> {
    return this.authService.validateUser(email, password);
  }
}


2FA Strategy


// twofa.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { AuthService } from './auth.service';

@Injectable()
export class TwoFaStrategy extends PassportStrategy(Strategy, '2fa') {
  constructor(private authService: AuthService) {
    super();
  }

  async validate(email: string, token: string): Promise<any> {
    return this.authService.validateTwoFa(email, token);
  }
}


Implementing Authentication Service


// auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { UsersService } from './users.service';
import { speakeasy } from 'speakeasy';

@Injectable()
export class AuthService {
  constructor(private usersService: UsersService) {}

  async validateUser(email: string, password: string): Promise<any> {
    const user = await this.usersService.findByEmail(email);
    if (user && user.password === password) {
      return user;
    }
    throw new UnauthorizedException();
  }

  async generateTwoFaSecret(email: string): Promise<string> {
    const secret = speakeasy.generateSecret({ length: 20 }).base32;
    await this.usersService.updateTwoFaSecret(email, secret);
    return secret;
  }

  async validateTwoFa(email: string, token: string): Promise<any> {
    const user = await this.usersService.findByEmail(email);
    if (!user || !user.secretKey) {
      throw new UnauthorizedException();
    }
    const isValidToken = speakeasy.totp.verify({
      secret: user.secretKey,
      encoding: 'base32',
      token,
      window: 1,
    });
    if (isValidToken) {
      return user;
    }
    throw new UnauthorizedException();
  }
}


Controller Endpoints

Below is an example of how you can implement controller endpoints for user registration, login, and enabling/disabling 2FA in a NestJS application:


// auth.controller.ts
import { Controller, Post, Body, Req, UseGuards, Get } from '@nestjs/common';
import { AuthService } from './auth.service';
import { CreateUserDto } from './dto/create-user.dto';
import { LocalAuthGuard } from './guards/local-auth.guard';
import { Request } from 'express';
import { TwoFaAuthGuard } from './guards/twofa-auth.guard';

@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post('register')
  async register(@Body() createUserDto: CreateUserDto) {
    return this.authService.register(createUserDto);
  }

  @Post('login')
  @UseGuards(LocalAuthGuard)
  async login(@Req() req: Request) {
    return this.authService.login(req.user);
  }

  @Post('twofa/enable')
  async enableTwoFa(@Req() req: Request) {
    return this.authService.enableTwoFa(req.user.email);
  }

  @Post('twofa/disable')
  @UseGuards(TwoFaAuthGuard)
  async disableTwoFa(@Req() req: Request) {
    return this.authService.disableTwoFa(req.user.email);
  }

  @Post('twofa/verify')
  @UseGuards(TwoFaAuthGuard)
  async verifyTwoFa(@Req() req: Request) {
    return this.authService.verifyTwoFa(req.user.email, req.body.token);
  }

  @Get('twofa/secret')
  @UseGuards(TwoFaAuthGuard)
  async getTwoFaSecret(@Req() req: Request) {
    return this.authService.getTwoFaSecret(req.user.email);
  }
}


In this example:

  • /auth/register​: This endpoint allows users to register by providing their email and password.
  • /auth/login​: This endpoint allows users to log in using their email and password.
  • /auth/twofa/enable​: This endpoint enables 2FA for the authenticated user.
  • /auth/twofa/disable​: This endpoint disables 2FA for the authenticated user.
  • /auth/twofa/verify​: This endpoint verifies the 2FA token provided by the user during login.
  • /auth/twofa/secret​: This endpoint retrieves the secret key for 2FA, which can be used for setting up authenticator apps like Google Authenticator.


Ensure that you have corresponding DTOs (such as CreateUserDto​) and guards (such as LocalAuthGuard​ and TwoFaAuthGuard​) defined to handle input validation and authentication logic properly. Also, make sure to implement the necessary service methods in AuthService​ to handle user registration, login, and 2FA-related operations.


DTOs

DTO stands for Data Transfer Object. In NestJS, DTOs are plain JavaScript/TypeScript objects used to define the structure of data being transferred between the client and server. They help ensure type safety, validate input data, and provide a clear contract between the client and server.

Here's an example of a DTO for user registration:


// create-user.dto.ts
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';

export class CreateUserDto {
  @IsNotEmpty()
  @IsEmail()
  email: string;

  @IsNotEmpty()
  @IsString()
  @MinLength(6)
  password: string;
}


In this DTO:

  • @IsNotEmpty()​: Ensures that the email and password fields are not empty.
  • @IsEmail()​: Validates that the email field is a valid email address.
  • @IsString()​: Validates that the password field is a string.
  • @MinLength(6)​: Specifies that the password must be at least 6 characters long.

You can define similar DTOs for other operations such as login, enabling/disabling 2FA, etc.


Summary:


Exploring the implementation of Two-Factor Authentication (2FA) in a NestJS application can be a valuable addition to your security measures. By utilizing the @nestjs/passport​ module in conjunction with the speakeasy​ library for generating and verifying one-time passwords (OTPs), you can bolster the protection of user accounts and fortify your application against potential security breaches. Incorporating 2FA​ not only enhances the overall security posture of your application but also instills a sense of trust and confidence among your users.


Hope you find this helpful!!!!

Do comment your suggestions and questions....

Two-Factor Authentication (2FA) in NestJS: A Comprehensive Guide
Ram Krishna April 10, 2024
Share this post
Our blogs
Sign in to leave a comment
Angular RxJS: A Comprehensive Guide