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- ToDo-App
and the code — GitHubN.B. Refresh token generator using the same application. Link- JWT-Token
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.