While developing our application, security should be one of our main concerns. One of the ways we can improve it is by implementing a two-factor authentication mechanism. This article goes through its principles and puts them into practice with NestJS and Google Authenticator. Adding two-factor authentication The core idea behind two-factor authentication is to confirm the user’s identity in two ways. There is an important distinction between two-step authentication and two-factor authentication. A common example is with the ATM. To use it, we need both a credit card and a PIN code. We call it a two-factor authentication because it requires both something we have and something we know. For example, requiring a password and a PIN code could be called a two-step flow instead. The very first thing is to create a secret key unique for every user. In the past, I’ve used the speakeasy library for that. Unfortunately, it is not maintained anymore. Therefore, in this article, we use the otplib package for this purpose.npm install otplibAlong with the above secret, we also generate a URL with the otpauth:// protocol. It is used by applications such as Google Authenticator. We need to provide a name for our application to display it on our users’ devices. To do that, let’s add an environment variable called TWO_FACTOR_AUTHENTICATION_APP_NAME. twoFactorAuthentication.service.ts import { Injectable } from '@nestjs/common'; import { authenticator } from 'otplib'; import User from '../../users/user.entity'; import { UsersService } from '../../users/users.service'; @Injectable() export class TwoFactorAuthenticationService { constructor ( private readonly usersService: UsersService, private readonly configService: ConfigService ) {} public async generateTwoFactorAuthenticationSecret(user: User) { const secret = authenticator.generateSecret(); const otpauthUrl = authenticator.keyuri(user.email, this.configService.get('TWO_FACTOR_AUTHENTICATION_APP_NAME'), secret); await this.usersService.setTwoFactorAuthenticationSecret(secret, user.id); return { secret, otpauthUrl } } }An essential thing above is that we save the generated secret in the database. We will need it later. user.entity.ts import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; @Entity() class User { @PrimaryGeneratedColumn() public id: number; @Column({ nullable: true }) public twoFactorAuthenticationSecret?: string; // ... } export default User; user.service.ts import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import User from './user.entity'; @Injectable() export class UsersService { constructor( @InjectRepository(User) private usersRepository: Repository, ) {} async setTwoFactorAuthenticationSecret(secret: string, userId: number) { return this.usersRepository.update(userId, { twoFactorAuthenticationSecret: secret }); } // ... }We also need to serve the otpauth URL to the user in a QR code. To do that, we can use the qrcode library. twoFactorAuthentication.service.ts import { Injectable } from '@nestjs/common'; import { toFileStream } from 'qrcode'; import { Response } from 'express'; @Injectable() export class TwoFactorAuthenticationService { // ... public async pipeQrCodeStream(stream: Response, otpauthUrl: string) { return toFileStream(stream, otpauthUrl); } }Once we have all of the above, we can create a controller that uses this logic. twoFactorAuthentication.controller.ts import { ClassSerializerInterceptor, Controller, Header, Post, UseInterceptors, Res, UseGuards, Req, } from '@nestjs/common'; import { TwoFactorAuthenticationService } from './twoFactorAuthentication.service'; import { Response } from 'express'; import JwtAuthenticationGuard from '../jwt-authentication.guard'; import RequestWithUser from '../requestWithUser.interface'; @Controller('2fa') @UseInterceptors(ClassSerializerInterceptor) export class TwoFactorAuthenticationController { constructor( private readonly twoFactorAuthenticationService: TwoFactorAuthenticationService, ) {} @Post('generate') @UseGuards(JwtAuthenticationGuard) async register(@Res() response: Response, @Req() request: RequestWithUser) { const { otpauthUrl } = await this.twoFactorAuthenticationService.generateTwoFactorAuthenticationSecret(request.user); return this.twoFactorAuthenticationService.pipeQrCodeStream(response, otpauthUrl); } } Above, we use the RequestWithUser interface and require the user to be authenticated. If you want to know more about it, check out API with NestJS #3. Authenticating users with bcrypt, Passport, JWT, and cookies Calling the above endpoint results in the API returning a QR code. Our users can now scan it with the Google Authenticator application. Turning on the two-factor authentication So far, our users can generate a QR code and scan it with the Google Authenticator application. Now we need to implement the logic of turning on the two-factor authentication. It requires the user to provide the code from the Authenticator application. We then need to validate it against the secret string we’ve saved in the database while generating a QR code. We need to save the information about the two-factor authentication being turned on in the database. To do that, let’s expand the entity of the user. user.entity.ts import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; @Entity() class User { @PrimaryGeneratedColumn() public id: number; @Column({ default: false }) public isTwoFactorAuthenticationEnabled: boolean; // ... } export default User;We also need to create a method in the service to set this flag to true. users.service.ts import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import User from './user.entity'; @Injectable() export class UsersService { constructor( @InjectRepository(User) private usersRepository: Repository, ) {} async turnOnTwoFactorAuthentication(userId: number) { return this.usersRepository.update(userId, { isTwoFactorAuthenticationEnabled: true }); } // ... }The most crucial part here is verifying the user’s code against the secret saved in the database. Let’s do that in the TwoFactorAuthenticationService: twoFactorAuthentication.service.ts import { Injectable } from '@nestjs/common'; import { authenticator } from 'otplib'; import User from '../../users/user.entity'; @Injectable() export class TwoFactorAuthenticationService { public isTwoFactorAuthenticationCodeValid(twoFactorAuthenticationCode: string, user: User) { return authenticator.verify({ token: twoFactorAuthenticationCode, secret: user.twoFactorAuthenticationSecret }) } // ... }Once we’ve got all of the above ready to go, we can use this logic in our controller: twoFactorAuthentication.controller.ts import { ClassSerializerInterceptor, Controller, Post, UseInterceptors, UseGuards, Req, Body, UnauthorizedException, HttpCode, } from '@nestjs/common'; import { TwoFactorAuthenticationService } from './twoFactorAuthentication.service'; import JwtAuthenticationGuard from '../jwt-authentication.guard'; import RequestWithUser from '../requestWithUser.interface'; import { TurnOnTwoFactorAuthenticationDto } from './dto/turnOnTwoFactorAuthentication.dto'; import { UsersService } from '../../users/users.service'; @Controller('2fa') @UseInterceptors(ClassSerializerInterceptor) export class TwoFactorAuthenticationController { constructor( private readonly twoFactorAuthenticationService: TwoFactorAuthenticationService, private readonly usersService: UsersService ) {} @Post('turn-on') @HttpCode(200) @UseGuards(JwtAuthenticationGuard) async turnOnTwoFactorAuthentication( @Req() request: RequestWithUser, @Body() { twoFactorAuthenticationCode } : TwoFactorAuthenticationCodeDto ) { const isCodeValid = this.twoFactorAuthenticationService.isTwoFactorAuthenticationCodeValid( twoFactorAuthenticationCode, request.user ); if (!isCodeValid) { throw new UnauthorizedException('Wrong authentication code'); } await this.usersService.turnOnTwoFactorAuthentication(request.user.id); } // ... } Above, we create a Data Transfer Object with the twoFactorAuthenticationCode property. If you want to know more about how to create DTOs with validation, check out API with NestJS #4. Error handling and data validation Now, the user can generate a QR code, save it in the Google Authenticator application, and send a valid code to the /2fa/turn-on endpoint. If that’s the case, we acknowledge that the two-factor authentication has been saved. Logging in with two-factor authentication The next step in our two-factor authentication flow is allowing the user to log in. In this article, we implement the following approach: the user logs in using the email and the password, and we respond with a JWT token, if the 2FA is turned off, we give full access to the user, if the 2FA is turned on, we provide the access just to the /2fa/authenticate endpoint, the user looks up the Authenticator application code and sends it to the /2fa/authenticate endpoint; we respond with a new JWT token with full access. The first missing part of the above flow is the route that allows the user to send the two-factor authentication code. twoFactorAuthentication.controller.ts import { ClassSerializerInterceptor, Controller, Post, UseInterceptors, UseGuards, Req, Body, UnauthorizedException, HttpCode, } from '@nestjs/common'; import { TwoFactorAuthenticationService } from './twoFactorAuthentication.service'; import JwtAuthenticationGuard from '../jwt-authe