API with NestJS #31. Two-factor authentication

JavaScript NestJS TypeScript

This entry is part 31 of 121 in the API with NestJS

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.

Along with the above secret, we also generate a URL with the 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 .

twoFactorAuthentication.service.ts

An essential thing above is that we save the generated secret in the database. We will need it later.

user.entity.ts

user.service.ts

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

Once we have all of the above, we can create a controller that uses this logic.

twoFactorAuthentication.controller.ts

Above, we use the 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

We also need to create a method in the service to set this flag to .

users.service.ts

The most crucial part here is verifying the user’s code against the secret saved in the database. Let’s do that in the :

twoFactorAuthentication.service.ts

Once we’ve got all of the above ready to go, we can use this logic in our controller:

twoFactorAuthentication.controller.ts

Above, we create a Data Transfer Object with the 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 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:

  1. the user logs in using the email and the password, and we respond with a JWT token,
  2. if the 2FA is turned off, we give full access to the user,
  3. if the 2FA is turned on, we provide the access just to the endpoint,
  4. the user looks up the Authenticator application code and sends it to the 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

A crucial thing to notice above is that we’ve added an argument to the method.

authentication.service.ts

Thanks to setting the property, we can now distinguish between tokens created with and without two-factor authentication.

Checking if the user is authenticated with the second factor

Since we can authenticate users using the second factor, we now should check it before we grant them access to various resources. In the third part of this series, we’ve created a Passport strategy that parses the cookie and the JWT token. Let’s expand on this idea and create a strategy and a guard that check if the two-factor authentication was successful.

authentication.service.ts

Above, the crucial logic happens in the method. If the two-factor authentication is not enabled for the current user, we don’t check if the token contains the flag.

To use the above strategy, we need to create a guard:

jwt-two-factor.guard.ts

We can now use it on endpoints that we want to protect with two-factor authentication.

posts.controller.ts

It is crucial not to use the on the endpoint, because we need users to access it before authenticating with the second factor.

Modifying the basic logging-in logic

The last step is modifying the regular endpoint. It always responds with the user’s data, even if we didn’t perform two-factor authentication yet. Let’s change it.

authentication.controller.ts

Summary

In this article, we’ve implemented a fully working two-factor authentication flow. Our users can now generate a unique, secret key, and we present them with a QR image. After turning on the two-factor authentication, we validate upcoming requests.

The above approach might benefit from additional features. An example would be support for backup codes that the user could use in case of losing the phone. I encourage you to improve the flow presented in this article.

Series Navigation<< API with NestJS #30. Scalar types in GraphQLAPI with NestJS #32. Introduction to Prisma with PostgreSQL >>
Subscribe
Notify of
guest
7 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Maissen Ayed
Maissen Ayed
3 years ago

Thank you again for this awesome work ,i’m really learning a lot from these articles , but i have a question in mind , can we create a Api gateway that dynamically proxy all the Api’s of one microservice or multiple microservices, maybe a service discovery , if not do you you know any tools or books that can help achieve that with Nest js ???

saadi
saadi
1 year ago
Reply to  Maissen Ayed

You should read about Templating in OOP, as nestjs use OOP structure, classes and their concepts, Templates can help you make dynamic functions and will give you more than enough power.

rahsut
rahsut
3 years ago

In my swagger/Postman I can’t generate the QR code like you did. It’s sending me back some extracted png format. How can I show this in swagger/Postman ? Even I don’t know it is working or not. I know it’s unrecognized but is there any way to show the QRcode ?

rahsut
rahsut
3 years ago

I found a solution and it works for me which is – add a line stream.setHeader(‘content-type’,’image/png’); in pipeQrCodeStream()

Last edited 3 years ago by rahsut
Shamim
2 years ago
Reply to  rahsut

I think you should put that line on the controller level not the service level. For example on the @Post(‘generate’) add the below line before calling this.twoFactorAuthenticationService.pipeQrCodeStream,

response.setHeader(‘content-type’, ‘image/png’);

Loic
Loic
2 years ago

Thank you, great explanation.

Just a question how do I display the QR code on the website frontend.

Axios doesn’t seem to support fileStream in the browser and I’m not sure with Fetch how to go from a fileStream to a base64

Misir
Misir
2 years ago
Reply to  Loic

Set the responseType to blob. Then const url = URL.createObjectURL (response);

Angular code example:

async generateQr(): Promise<void> {
try {
const response = await this.http.post(this.baseUrl + '/auth/generate', {}, {
headers: {
Authorization: 'Bearer' +
${this.token}},
responseType: 'blob',
}).toPromise();
this.fileUrl = this.domSanitizer.bypassSecurityTrustResourceUrl(URL.createObjectURL(response));
}catch (e) {
console.log(e);
}
}