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 { Controller, Post, Req, UseGuards, Get, Body, BadRequestException, Param, NotFoundException, } ; { JwtAuthGuard } ; { LoggedInToken } ; { AuthService } ; * speakeasy ; { optSecret } ; { 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, } ; { plainToClass } ; { success } ; { UserDto } ; { OtpEmail, UserCycloanAccountBlockedEmail } ; { ForgetPasswordOtpEmail, PasswordChangedAlert, } ; { EmailService } ; { OtpService } ; { RequestUser } ; { UsersService } ; { EmailDto } ; { OtpDto } ; { InjectModel } ; { IOtp, Otp } ; { Model } ; { ForgotPasswordOtpService } ; { ForgotPasswordOtp } ; { ForgotPasswordOtpDto } ; { OtpIncorrectService } ; { OtpIncorrect } ; { BlockedAccountService } ; { IBlockedAccount } ; { OTP_RETRY_LIMIT, Status, ROLES_ACCESS_ACTION, BLOCKED_ACCOUNT_TYPE } ; { RolesService } ; { OtpIncorrectForgotPasswordService } ; { OtpIncorrectForgotPassword } ; @Controller( ) { ( 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 refresh(@Req() req): { .authService.createJwtPayLoad(req.user); } @Get( ) getSecret() { secret = speakeasy.generateSecret({ : }); secret; } @Post( ) getOtp( @Req() req, @Body() body: { : string; firstName: string; lastName: string } ) { ; email = body.email; firstName = body.firstName; lastName = body.lastName; token = speakeasy.totp({ : optSecret, : , }); userToAttempt: any = .usersService.findOneByEmail(body.email); (!userToAttempt) { _blocked: any = .blockedAccountService.findOneByQuery({ : email, : BLOCKED_ACCOUNT_TYPE.USER_REGISTRATION}) (_blocked !== ){ BadRequestException(BLOCKED_ACCOUNT_ERROR(email)) } query = { : email }; _otp: any = .otpService.findOneByQuery(query); currentTime: number = .now(); (_otp) { k: any = .otpModel .find({ : email }) .sort({ : }) .limit( ); (k !== ) { diff = (currentTime - k[ ].expiry) / ; updateTime: number = .now(); createDto: any = { : token, : email, : firstName, : lastName, : updateTime + * * , }; (diff > ) { _otp: any = .otpService.create(createDto); _data = + body.email + + + token; .emailService.sendEmail( OtpEmail( EmailDto({ : body.email, : { email, token, firstName, lastName }, }) ) ); success(_data); } { errorData = + diff + ; BadRequestException(OTP_NOT_EXPIRED(errorData)); } } } updateTime: number = .now(); createDto: any = { : token, : email, : updateTime + * * , }; _otp1: any = .otpService.create(createDto); .emailService.sendEmail( OtpEmail( EmailDto({ : body.email, : { email, token, firstName, lastName }, }) ) ); _data1 = + body.email + + + token; success(_data1); } BadRequestException(EXISTS, ); } } import from "@nestjs/common" import from "./auth.guard" import from "../users/objects/login-user.dto" import from "./auth.service" import as from "speakeasy" import from "../common/constants/config" import from "../common/constants/string" import from "class-transformer" import from "../common/base/httpResponse.interface" import from "../users/objects/create-user.dto" import from "../users/objects/user.registered.email" import from "../users/objects/user.registered.email" import from "../email/email.service" import from "./otp/otp.service" import from "../common/utils/controller.decorator" import from "../users/users.service" import from "../email/objects/email.dto" import from "./otp/otp.dto" import from "@nestjs/mongoose" import from "./otp/otp.schema" import from "mongoose" import from "./forgot-password-otp/forgot-password-otp.service" import from "./forgot-password-otp/forgot-password-otp.schema" import from "./forgot-password-otp/forgot-password-otp.dto" import from "./otpIncorrect/otpIncorrect.service" import from "./otpIncorrect/otpIncorrect.schema" import from "./blockedAccounts/blockedAccounts.service" import from "./blockedAccounts/blockedAccounts.schema" import from "../common/constants/enum" import from "../roles/roles.service" import from "./otpIncorrectForgotPassword/otpIncorrectForgotPassword.service" import from "./otpIncorrectForgotPassword/otpIncorrectForgotPassword.schema" //@UseGuards(JwtAuthGuard) "auth/refresh" export class AuthController constructor async Promise return this //Api For generating a secret and storing it in config.ts "secret" async const length 20 return //Api For generating a 6 digit token using the secret "generate" async email //@RequestUser() user debugger let let let var secret encoding "base32" let await this //Check for existing users if let await this email type if null throw new let email let await this let Date if let await this email updatedTime -1 1 if undefined let 0 1000 let Date let token email firstName lastName expiry 15 60 1000 if 0 let await this let "Otp sent to registered email " " " "token:" await this new new to metaData return else let "Otp sent yet to expire in" "seconds" throw new //For users requesting for the first time let Date let token email expiry 15 60 1000 let await this await this new new to metaData let "Otp sent to registered email " " " "token:" return throw new "User exists" In the first method, below, @Get( ) getSecret() { secret = speakeasy.generateSecret({ : }); secret; } "secret" async const length 20 return Here, we create a secret and store it in the config.ts file(not recommended). src/common/constants/config.ts * dotenv ; dotenv.config(); optSecret = ; import as from "dotenv" export const "HJCCU6Z7NNAS4UCHMJFHOI3YN47UYS2C" After storing the secret, the OTP is generated by calling the POST Api by sending the email for which OTP needs to be sent in the body of the request as shown below. http://localhost:3000/api/v1/auth/refresh/generate { : } "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: .otpService.findOneByQuery(query); this 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. token = speakeasy.totp({ : optSecret, : , }); createDto: any = { : token, : email, : updateTime + * * , }; _otp1: any = .otpService.create(createDto); .emailService.sendEmail( OtpEmail( EmailDto({ : body.email, : { email, token, firstName, lastName }, }) ) ); var secret encoding "base32" let token email expiry 15 60 1000 let await this await this new new to metaData 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". (diff > ) { _otp: any = .otpService.create(createDto); _data = + body.email + + + token; .emailService.sendEmail( OtpEmail( EmailDto({ : body.email, : { email, token, firstName, lastName }, }) ) ); success(_data); } { errorData = + diff + ; BadRequestException(OTP_NOT_EXPIRED(errorData)); } if 0 let await this let "Otp sent to registered email " " " "token:" await this new new to metaData return else let "Otp sent yet to expire in" "seconds" throw new 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. tokenValidates = speakeasy.totp.verify({ : optSecret, : , : otp, : , }); (tokenValidates) { update = { : , }; } { ... } updated = .otpService.edit(_otp.id, update, updateTime); _data = plainToClass(OtpDto, updated, { : , }); success(_data); var secret encoding "base32" token window 30 if isVerified true else let await this const excludeExtraneousValues true return 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" (otpErrorCount > OTP_RETRY_LIMIT.MAXIMUM_OTP_RETRY_LIMIT){ _blocked: any = .blockedAccountService.findOneByQuery({ : email, : BLOCKED_ACCOUNT_TYPE.USER_REGISTRATION}) (_blocked == ){ _blocked: any = .blockedAccountService.create(createBlockedAccountDto); .emailService.sendEmail( UserCycloanAccountBlockedEmail( EmailDto({ : body.email, : { email, }, }) ) ); .log( ); } .log( , _blocked); BadRequestException(BLOCKED_ACCOUNT_ERROR(email)) } BadRequestException(OTP_ERROR); } if let await this email type if null let await this //console.log('Your account is added to blocked list. BLOCKED LIST BLOCKED LIST BLOCKED LIST', _blocked); await this new new to metaData //firstName, lastName console 'Blocked Account email sent.................' console 'Your account is added to blocked list. BLOCKED LIST BLOCKED LIST BLOCKED LIST' throw new throw new The entire code for email verification is given below: @Post( ) verifyOTP( @Param( ) emailOrMobile, @Body() body: { : string; email: string } ) { ; otp = body.otp; email = body.email; updateTime: number = .now(); update = {}; _blocked: any = .blockedAccountService.findOneByQuery({ : email, : BLOCKED_ACCOUNT_TYPE.USER_REGISTRATION}) .log( , ,_blocked); (_blocked !== ){ BadRequestException(BLOCKED_ACCOUNT_ERROR(email)) } userToAttempt: any = .usersService.findOneByEmail(email); (!userToAttempt) { query = { : otp, : email }; _otp: any = .otpService.findOneByQuery(query); (emailOrMobile) { : update = { : }; ; : tokenValidates = speakeasy.totp.verify({ : optSecret, : , : otp, : , }); (tokenValidates) { update = { : , }; } { updateTime: number = .now(); createDto: any = { : otp, : email }; createBlockedAccountDto: any = { : email, : BLOCKED_ACCOUNT_TYPE.USER_REGISTRATION } _otp: any = .otpIncorrectService.create(createDto); .log( , _otp) otpErrorCount: any = .otpIncorrectModel.count({ : email}); .log( ,otpErrorCount, ) (otpErrorCount > OTP_RETRY_LIMIT.MAXIMUM_OTP_RETRY_LIMIT){ _blocked: any = .blockedAccountService.findOneByQuery({ : email, : BLOCKED_ACCOUNT_TYPE.USER_REGISTRATION}) (_blocked == ){ _blocked: any = .blockedAccountService.create(createBlockedAccountDto); .emailService.sendEmail( UserCycloanAccountBlockedEmail( EmailDto({ : body.email, : { email, }, }) ) ); .log( ); } .log( , _blocked); BadRequestException(BLOCKED_ACCOUNT_ERROR(email)) } BadRequestException(OTP_ERROR); } ; : BadRequestException(UNKNOWN_PARAM(emailOrMobile)); } updated = .otpService.edit(_otp.id, update, updateTime); _data = plainToClass(OtpDto, updated, { : , }); success(_data); } } //Api for verifying a 6 digit token using the secret "otp/:emailOrMobile" async "emailOrMobile" otp debugger let let let Date let let await this email type console '_blocked' '_blocked .................._blocked' if null throw new const await this if let token email let await this switch case "mobile" mobile true break case "email" var secret encoding "base32" token window 30 if isVerified true else let Date let token email let email type //if (diff > 0) { let await this console 'otp tokennnnnnnnnn errorrrr' let await this email console 'Otp error count' 'If the attempts of failure are greater than 10, block this account. Create blockedCollection.' if let await this email type if null let await this //console.log('Your account is added to blocked list. BLOCKED LIST BLOCKED LIST BLOCKED LIST', _blocked); await this new new to metaData //firstName, lastName console 'Blocked Account email sent.................' console 'Your account is added to blocked list. BLOCKED LIST BLOCKED LIST BLOCKED LIST' throw new throw new break default throw new let await this const excludeExtraneousValues true return Link to code: https://gitlab.com/adh.ranjan/nestjs/-/tree/dSuahailTwo Previously published at https://dev.to/krishnakurtakoti/otp-genertion-and-verification-using-speakeasy-nest-js-and-mongodb-4nam
Share Your Thoughts