2FA in NestJS Application

Hello everyone ! I am going to describe Two Factor Authentication(2FA) in NestJS using a Todo application. So let’s jump in.

N.B. I already wrote this tutorial series for Todo application using — NestJS, JWT, PostgresSQL, PgAdmin4 and Docker. Feel free to look at it. Link- https://tushar-chy.medium.com/a-simple-todo-application-with-nestjs-typeorm-postgresql-swagger-pgadmin4-jwt-and-docker-caa2742a4295
and the code — GitHub

N.B. Refresh token generator using the same application. Link- https://tushar-chy.medium.com/jwt-refresh-token-generator-in-nestjs-application-54c5ab2c0da3
I recommand to understand the Refresh-token-generator. It will be better for this tutorial and code — GitHub

We are going to use otplib for secret generator & qrcode for keyUri. So let’s run the below command

// under docker env
docker-compose run nestjs npm install otplib grcode
// without docker env
npm install otplib grcode

First, let’s update the .env file and a line

TWO_FACTOR_AUTHENTICATION_APP_NAME=todo-app

And also add a line in config/development.yml (Those who are not going to use docker as their environment)

jwt:
...
twoFactorAppName: 'todo-app'

Now we need to store 2FA secret code & the visibility of 2FA in DB. So let’s update the auth/entity/user.entity.ts file

...
@Column({ nullable: true })
twoFactorAuthSecret?: string
@Column({ default: false })
public isTwoFactorEnable: boolean
...

Let’s do the migration using this below command

// under docker environment
docker-compose run nestjs npm run typeorm:generate anyNameYouLike
docker-compose run nestjs npm run typeorm:run
// without docker environment
npm run typeorm:generate anyNameYouLike
npm run typeorm:run

N.B. You have to empty the migration folder before you run the above command and if you have stale migration files.

update the auth/interface/jwt-payload.interface.ts file

export interface JwtPayload {
isTwoFactorEnable?: boolean
...
isTwoFaAuthenticated?: boolean
}

Update the validateUserPassword() in auth/repository/user.repository.ts and the signIn() in auth/service/auth.service.ts files and create auth/dto/two-fa-auth.dto.ts file

Create src/auth/strategy/jwt-2fa-strategy.ts, auth/service/two-factor-auth.service.ts, auth/controller/two-factor-auth.controller.ts & guards/jwt-two-factor.guard.ts fileguards/jwt-two-factor.guard.ts

As you noticed that, We didn’t update @UseGuards(JwtAuthenticationGuard) To @UseGuards(JwtTwoFactorGuard). Because if we use JwtTwoFactorGuard then we will not able to generate QR-code. The concept is-
1. After sign-in we will get the accessToken
2. With that token we will call /generate-qr API to generate QR which will be scanned by the google-authenticator application and generate codes.
3. After that we will call /turn-on-qr to turn on the QR as well as check the inserted code from the google-authenticator app in /authenticate API.
4. If those steps are correct then it will sign-in again & set the JwtTwoFactorGuard all over the application.
5. After successful login we don’t need to generate the QR code frequently. Just get the code & submit it through /authenticate API.

N.B. You will find those logics at the below codes

Update the validate() in auth/strategy/jwt-refresh-strategy.ts file

async validate(payload: JwtPayload) {
const { username } = payload;
const user = await this.userRepository.findOne({ username });
if (!user) {
throw new UnauthorizedException();
}
if (!user.isTwoFactorEnable) {
return user;
}
if (payload.isTwoFaAuthenticated) {
return user;
}
}

Update todo/todo.controller.ts & user/user.controller.ts file

update 
@UseGuards(JwtAuthenticationGuard) To @UseGuards(JwtTwoFactorGuard)

Update auth/controller/auth.controller.ts file

...
@Post('/signin')
signIn(
@Body(ValidationPipe) signinCredentialsDto: SignInCredentialsDto
): Promise<{ accessToken: string, refreshToken?: string, user?: JwtPayload }>{
return this.authService.signIn(signinCredentialsDto);
}
...
@UseGuards(JwtTwoFactorGuard)
@Get('/logout')
...
@ApiBearerAuth()
@UseGuards(JwtRefreshTokenGuard)
@Post('/refresh-token')
async refreshToken(
@GetUser() user: User,
@Body() token: RefreshTokenDto
){
const user_info = await this.authService.getUserIfRefreshTokenMatches(token.refresh_token, user.username)
if (user_info) {
const userInfo = {
username: user_info.username,
user_info: user_info.user_info
}
if (user.isTwoFactorEnable) {
userInfo['isTwoFaAuthenticated'] = true;
userInfo['isTwoFactorEnable'] = user.isTwoFactorEnable;

}
return this.authService.getNewAccessAndRefreshToken(userInfo)
} else{
return null
}
}

And update auth/auth.module.ts file

controllers: [AuthController, TwoFactorAuth],
providers: [
...
TwoFactorAuthService,
...
JwtTwoFaStrategy
],
exports: [
...
JwtTwoFaStrategy
]

So this is it. Just play around with swagger/postman And obviously the code in GitHub. I think this tutorial might help you.

Thank You !!!

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Tushar Roy Chowdhury

Tushar Roy Chowdhury

63 Followers

I am a passionate programmer and always keep eye on new technologies and scrutinize them until getting into shape.