Otp genertion and verification using speakeasy, Nest.js and MongoDB

Here, we have designed and developed a flow for OTP(One time password) for user registration and also blocking a user's account after the maximum retries for incorrect otp is exceeded.
We will go in steps for generation, verification and blocking a user's account.

Step 1:OTP Generation:Code (Part-I):src/auth/auth.controller.ts
import {
  Controller,
  Post,
  Req,
  UseGuards,
  Get,
  Body,
  BadRequestException,
  Param,
  NotFoundException,
} from "@nestjs/common";
import { JwtAuthGuard } from "./auth.guard";
import { LoggedInToken } from "../users/objects/login-user.dto";
import { AuthService } from "./auth.service";
import * as speakeasy from "speakeasy";
import { optSecret } from "../common/constants/config";
import {
  UNKNOWN_PARAM,
  EMAIL_NOT_FOUND,
  OTP_ERROR,
  EXISTS,
  OTP_NOT_EXPIRED,
  NEW_PASSWORD_AND_CONFIRM_NEW_PASSWORD_ERROR,
  OTP_TIME_OUT,
  TOKEN_ALREADY_USED,
  EMAIL_ERROR,
  BLOCKED_ACCOUNT_ERROR,
} from "../common/constants/string";
import { plainToClass } from "class-transformer";
import { success } from "../common/base/httpResponse.interface";
import { UserDto } from "../users/objects/create-user.dto";
import { OtpEmail, UserCycloanAccountBlockedEmail } from "../users/objects/user.registered.email";
import {
  ForgetPasswordOtpEmail,
  PasswordChangedAlert,
} from "../users/objects/user.registered.email";
import { EmailService } from "../email/email.service";
import { OtpService } from "./otp/otp.service";
import { RequestUser } from "../common/utils/controller.decorator";
import { UsersService } from "../users/users.service";
import { EmailDto } from "../email/objects/email.dto";
import { OtpDto } from "./otp/otp.dto";
import { InjectModel } from "@nestjs/mongoose";
import { IOtp, Otp } from "./otp/otp.schema";
import { Model } from "mongoose";
import { ForgotPasswordOtpService } from "./forgot-password-otp/forgot-password-otp.service";
import { ForgotPasswordOtp } from "./forgot-password-otp/forgot-password-otp.schema";
import { ForgotPasswordOtpDto } from "./forgot-password-otp/forgot-password-otp.dto";
import { OtpIncorrectService } from "./otpIncorrect/otpIncorrect.service";
import { OtpIncorrect } from "./otpIncorrect/otpIncorrect.schema";
import { BlockedAccountService } from "./blockedAccounts/blockedAccounts.service";
import { IBlockedAccount } from "./blockedAccounts/blockedAccounts.schema";
import { OTP_RETRY_LIMIT, Status, ROLES_ACCESS_ACTION, BLOCKED_ACCOUNT_TYPE } from "../common/constants/enum";
import { RolesService } from "../roles/roles.service";
import { OtpIncorrectForgotPasswordService } from "./otpIncorrectForgotPassword/otpIncorrectForgotPassword.service";
import { OtpIncorrectForgotPassword } from "./otpIncorrectForgotPassword/otpIncorrectForgotPassword.schema";

//@UseGuards(JwtAuthGuard)
@Controller("auth/refresh")
export class AuthController {
  constructor(
    private authService: AuthService,
    private emailService: EmailService,
    private usersService: UsersService,
    private otpService: OtpService,
    private forgotPasswordOtpService: ForgotPasswordOtpService,
    @InjectModel("Otp") private readonly otpModel: Model,
    @InjectModel("ForgotPasswordOtp")
    private readonly forgotPasswordotpModel: Model,
    private readonly otpIncorrectService: OtpIncorrectService,
    @InjectModel("OtpIncorrect") private readonly otpIncorrectModel: Model,
    private readonly blockedAccountService: BlockedAccountService,
    @InjectModel("BlockedAccount") private readonly blockedAccountModel: Model,
    private rolesservice: RolesService,
    private otpIncorrectForgotPasswordService: OtpIncorrectForgotPasswordService,
    @InjectModel("OtpIncorrectForgotPassword") private readonly otpIncorrectForgotPasswordModel: Model,
  ) {}

@UseGuards(JwtAuthGuard)
  @Post()
  public async refresh(@Req() req): Promise {
    return this.authService.createJwtPayLoad(req.user);
  }

//Api For generating a secret and storing it in config.ts
  @Get("secret")
  async getSecret() {
    const secret = speakeasy.generateSecret({ length: 20 });
    return secret;
  }
  //Api For generating a 6 digit token using the secret

@Post("generate")
  async getOtp(
    @Req() req,
    @Body() body: { email: string; firstName: string; lastName: string }
    //@RequestUser() user
  ) {
    debugger;
    let email = body.email;
    let firstName = body.firstName;
    let lastName = body.lastName;
    var token = speakeasy.totp({
      secret: optSecret,
      encoding: "base32",
    });
let userToAttempt: any = await this.usersService.findOneByEmail(body.email);

//Check for existing users
if (!userToAttempt) {

 let _blocked: any = await this.blockedAccountService.findOneByQuery({email: email, type: BLOCKED_ACCOUNT_TYPE.USER_REGISTRATION})

 if(_blocked !== null){
    throw new BadRequestException(BLOCKED_ACCOUNT_ERROR(email))
  }

  let query = { email: email };

  let _otp: any = await this.otpService.findOneByQuery(query);
  let currentTime: number = Date.now();
  if (_otp) {
    let k: any = await this.otpModel
      .find({ email: email })
      .sort({ updatedTime: -1 })
      .limit(1);
    if (k !== undefined) {
      let diff = (currentTime - k[0].expiry) / 1000;

      let updateTime: number = Date.now();
      let createDto: any = {
        token: token,
        email: email,
        firstName: firstName,
        lastName: lastName,
        expiry: updateTime + 15 * 60 * 1000,
      };
      if (diff > 0) {
        let _otp: any = await this.otpService.create(createDto);
        let _data =
          "Otp sent to registered email " +
          body.email +
          " " +
          "token:" +
          token;
        await this.emailService.sendEmail(
          new OtpEmail(
            new EmailDto({
              to: body.email,
              metaData: { email, token, firstName, lastName },
            })
          )
        );
        return success(_data);
      } else {
        let errorData = "Otp sent yet to expire in" + diff + "seconds";
        throw new BadRequestException(OTP_NOT_EXPIRED(errorData));
      }
    }
  }
  //For users requesting for the first time
  let updateTime: number = Date.now();
  let createDto: any = {
    token: token,
    email: email,
    expiry: updateTime + 15 * 60 * 1000,
  };
  let _otp1: any = await this.otpService.create(createDto);
  await this.emailService.sendEmail(
    new OtpEmail(
      new EmailDto({
        to: body.email,
        metaData: { email, token, firstName, lastName },
      })
    )
  );
  let _data1 =
    "Otp sent to registered email " + body.email + " " + "token:" + token;
  return success(_data1);
}
throw new BadRequestException(EXISTS, "User exists");

}
}

In the first method, below,

@Get("secret")
  async getSecret() {
    const secret = speakeasy.generateSecret({ length: 20 });
    return secret;
  }

Here, we create a secret and store it in the config.ts file(not recommended).

src/common/constants/config.ts

import * as dotenv from "dotenv";
dotenv.config();
  export const optSecret = "HJCCU6Z7NNAS4UCHMJFHOI3YN47UYS2C";

After storing the secret, the OTP is generated by calling the POST http://localhost:3000/api/v1/auth/refresh/generate Api
by sending the email for which OTP needs to be sent in the body of the request as shown below.

   
{
    "email": "az@gmail.com"
}

This is how the OTP generation flow follows:

We are first verifying if OTP is already generated and is not expired using the line below:

this.otpService.findOneByQuery(query);

If no Otp record exists for the user with the given email, we infer that the user is new user requesting Otp for the first time.We directly create a Otp record in the database and generate the otp token and send it to the requested user's email account.

    
 var token = speakeasy.totp({
      secret: optSecret,
      encoding: "base32",
    });

let createDto: any = {
        token: token,
        email: email,
        expiry: updateTime + 15 * 60 * 1000,
      };
     let _otp1: any = await this.otpService.create(createDto);
      await this.emailService.sendEmail(
        new OtpEmail(
          new EmailDto({
            to: body.email,
            metaData: { email, token, firstName, lastName },
          })
        )
      );   


  

If a Otp record already exists for the user's email, we will find the latest Otp record with the user's email and add a condition to check if the Otp is yet to expire. If the Otp has not expired at the time of sending a Otp generation request again, then will show an alert as "Otp sent yet to expire in" + diff + "seconds".

if (diff > 0) {
            let _otp: any = await this.otpService.create(createDto);
            let _data =
              "Otp sent to registered email " +
              body.email +
              " " +
              "token:" +
              token;
            await this.emailService.sendEmail(
              new OtpEmail(
                new EmailDto({
                  to: body.email,
                  metaData: { email, token, firstName, lastName },
                })
              )
            );
            return success(_data);
        }
       else {
       let errorData = "Otp sent yet to expire in" + diff + "seconds";
            throw new BadRequestException(OTP_NOT_EXPIRED(errorData));
       }


  

Step 2:
OTP Verification:

The Otp token and the email are sent as json in the body of the request for Otp verification in the api
POST http://localhost:3000/api/v1/auth/refresh/otp/email

    
{
    "email": "az@gmail.com",
    "otp": "124583"
}

We will verify that the email sent does not already exist in our user's database.We will then validate the token.If the token is verified, then we update the Otp record with the verified field as true

and return the success data.

    
var tokenValidates = speakeasy.totp.verify({
            secret: optSecret,
            encoding: "base32",
            token: otp,
            window: 30,
          });
          if (tokenValidates) {
            update = {
              isVerified: true,
            };
      } else {
          ...

       }

let updated = await this.otpService.edit(_otp.id, update, updateTime);
      const _data = plainToClass(OtpDto, updated, {
        excludeExtraneousValues: true,
      });
      return success(_data);

If the Otp is incorrect, we create a OtpIncorrect record and then count for the number of OtpIncorrect records bearing the user's email, then, check for the condition ,
count is greater than the Maximum retry limit.
If the condition is true, we will block the user by creating a record in the blocked list and return "user in the blocked list" error, else we will return "Otp error"

  
if(otpErrorCount > OTP_RETRY_LIMIT.MAXIMUM_OTP_RETRY_LIMIT){

let _blocked: any = await this.blockedAccountService.findOneByQuery({email: email, type: BLOCKED_ACCOUNT_TYPE.USER_REGISTRATION})
              if(_blocked == null){
                let _blocked: any = await this.blockedAccountService.create(createBlockedAccountDto);
                //console.log('Your account is added to blocked list. BLOCKED LIST BLOCKED LIST BLOCKED LIST', _blocked);
            await this.emailService.sendEmail(
              new UserCycloanAccountBlockedEmail(
                new EmailDto({
                  to: body.email,
                  metaData: { email, //firstName, lastName 
                  },
                })
              )
            );
            console.log('Blocked Account email sent.................');
          }
          console.log('Your account is added to blocked list. BLOCKED LIST BLOCKED LIST BLOCKED LIST', _blocked);
          throw new BadRequestException(BLOCKED_ACCOUNT_ERROR(email))
        }
        throw new BadRequestException(OTP_ERROR);
    }

The entire code for email verification is given below:

//Api for verifying a 6 digit token using the secret
  @Post("otp/:emailOrMobile")
  async verifyOTP(
    @Param("emailOrMobile") emailOrMobile,
    @Body() body: { otp: string; email: string }
  ) {
    debugger;
    let otp = body.otp;
    let email = body.email;
    let updateTime: number = Date.now();
    let update = {};
    let _blocked: any = await this.blockedAccountService.findOneByQuery({email: email, type: BLOCKED_ACCOUNT_TYPE.USER_REGISTRATION})
    console.log('_blocked','_blocked .................._blocked',_blocked); 
    if(_blocked !== null){
      throw new BadRequestException(BLOCKED_ACCOUNT_ERROR(email))
    }
    const userToAttempt: any = await this.usersService.findOneByEmail(email);
if (!userToAttempt) {
  let query = { token: otp, email: email };

  let _otp: any = await this.otpService.findOneByQuery(query);

  switch (emailOrMobile) {
    case "mobile":
      update = { mobile: true };
      break;
    case "email":
      var tokenValidates = speakeasy.totp.verify({
        secret: optSecret,
        encoding: "base32",
        token: otp,
        window: 30,
      });

      if (tokenValidates) {
        update = {
          isVerified: true,
        };

      } else {
        let updateTime: number = Date.now();
        let createDto: any = {
          token: otp,
          email: email
        };
        let createBlockedAccountDto: any = {
          email: email,
          type: BLOCKED_ACCOUNT_TYPE.USER_REGISTRATION
        }
        //if (diff > 0) {
        let _otp: any = await this.otpIncorrectService.create(createDto);
        console.log('otp tokennnnnnnnnn errorrrr', _otp)
        let otpErrorCount: any = await this.otpIncorrectModel.count({ email: email});
        console.log('Otp error count',otpErrorCount, 'If the attempts of failure are greater than 10, block this account. Create blockedCollection.')
        if(otpErrorCount > OTP_RETRY_LIMIT.MAXIMUM_OTP_RETRY_LIMIT){
          let _blocked: any = await this.blockedAccountService.findOneByQuery({email: email, type: BLOCKED_ACCOUNT_TYPE.USER_REGISTRATION})
          if(_blocked == null){
            let _blocked: any = await this.blockedAccountService.create(createBlockedAccountDto);
            //console.log('Your account is added to blocked list. BLOCKED LIST BLOCKED LIST BLOCKED LIST', _blocked);

            await this.emailService.sendEmail(
              new UserCycloanAccountBlockedEmail(
                new EmailDto({
                  to: body.email,
                  metaData: { email, //firstName, lastName 
                  },
                })
              )
            );
            console.log('Blocked Account email sent.................');
          }
          console.log('Your account is added to blocked list. BLOCKED LIST BLOCKED LIST BLOCKED LIST', _blocked);
          throw new BadRequestException(BLOCKED_ACCOUNT_ERROR(email))
        }
        throw new BadRequestException(OTP_ERROR);

      }
      break;
    default:
      throw new BadRequestException(UNKNOWN_PARAM(emailOrMobile));
  }
  let updated = await this.otpService.edit(_otp.id, update, updateTime);
  const _data = plainToClass(OtpDto, updated, {
    excludeExtraneousValues: true,
  });
  return success(_data);
}

}

Link to code: [Link]https://gitlab.com/adh.ranjan/nestjs/-/tree/dSuahailTwo