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).

As you are here, we request you to follow us on LinkedIn: Click here to follow  This help us creating new contents and you will get update also.

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.

Your Dynamic Snippet will be displayed here... This message is displayed because you did not provided both a filter and a template to use.


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 {     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 {     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 {     const user = await this.usersService.findByEmail(email);     if (user && user.password === password) {       return user;     }     throw new UnauthorizedException();   }   async generateTwoFaSecret(email: string): Promise {     const secret = speakeasy.generateSecret({ length: 20 }).base32;     await this.usersService.updateTwoFaSecret(email, secret);     return secret;   }   async validateTwoFa(email: string, token: string): Promise {     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....

For consultations, please email [email protected]

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