Two-factor authentication in Node.js – Stateless Application

Two-factor authentication is nowadays quite popular for having an added layer of security in web applications. While this increases the steps to log in, it ensures that your account is secure and can only be accessed by the rightful owner.

If you have any account that is of High value to you, you must definitely enable Two-factor Authentication. Major software providers like Google, Amazon, etc. support two-factor authentication. In fact, there is a website called twofactorauth.org which lists down websites and shows if they support Two-factor authentication.

How does Two-factor Authentication works?

Normally, to log-in to an application you are required to provide a password. In the case of Two-factor authentication, you are also required to enter a temporary one-time password (sometimes referred as a token) in addition to the usual password. This OTP can be provided to you in various ways. The ways in which the OTP is delivered defines the different types of 2fa. The OTP can be provided via email, SMS, as a software token using apps like Google Authenticator or Authy, it can also come via Hardware tokens. OTPs generated via software and hardware token are usually referred as TOTP (Time-Based OTP). Hardware tokens work in a similar way to software token, the difference being that you have a dedicated piece of hardware solely responsible for generating tokens.

The Flow

The user is asked to enter the username and password.

The system verifies against the given credentials.

On successful verification, the user is shown a screen to enter the OTP.

The user enter’s the OTP which was either sent via email/sms or from the software/hardware token generator.

The system verifies if the entered OTP is valid.

On successful validation the system logs in the user.

This can be better understood with the below flowcharts.

Flow-chart

The State-less dilemma

In a truly stateless application, the system does not maintain any state of the user. Each and every request is treated individually, things like tokens are used to validate the user. This validation is done on every request.

This statelessness in a RESTful application contradicts with how two-factor authentication works. In 2fa you need to store some kind of an intermediate state of the user between the time when the user enters the username/password until she enters the OTP.

An Opinionated Approach

There are a few ways by which you can implement 2fa in a stateless RESTful application.

Opaque Token: On the success of step1 send an opaque token to the client. The client must then return the token along with the OTP. The opaque token will help the system identify the user making the request.

Resend Credentials: On step 1 verify the credentials. If successful the UI redirects the user to enter the OTP. Here the user only enters the OTP but the client makes sure that the user credentials are re-sent along with the OTP.

I prefer the second approach. Well, this is quite opinionated. Approach 1 would also work well but then you have to deal with all the hassles of setting appropriate validity of the opaque token, invalidating the token as soon as OTP verification is done.

Where as in the second approach you can deliver a seamless experience to the user and also avoid too many complications in the implementation.

Our Application

Enough theory, now it’s time to get our hands dirty and try to implement this. We will build a small authentication application that has two-factor enabled.

Before we jump into implementation here are a few points to note.

We will build our application in Node.js.

ExpressJS will be our framework for building APIs.

Implementing standard two-factor auth using SMS is quite straight forward, so we won’t cover that here. But I’ll mention the flow.

We will add TOTP based two-factor authentication and use apps like Google Authenticator.

Just requiring a few modules and creating an express server. For demo purpose, we have an in-memory single user. We will use this user as an example. This is only to demonstrate the functionality, in a real application you will have multiple users stored in the database.

Let us now focus on building APIs. We will have 5 APIs. Obviously, we will need one login API. Then we will need two APIs to enable two-factor authentication for a user, one API to set it up and another to do a one-time verification. Next will also need an API to get two-factor setup details and finally one to disable two-factor authentication. Let’s add these, one by one.

This API will setup Two-factor for a logged in user. Note. we have not implemented session in this application, but in a real application, the respective user must be logged in.

TOTP is generated based on the combination of a secret key and current time. The secret key is usually a random base32 encoded string.
In the above API we are using speakeasy.generateSecret function to generate the secret key. Along with the secret key, the function also returns the otpauth_url, this can be used to generate a data_url which when added to an image tag will display the QRCode.
To get the dataURL we are using the qrcode module. Once we get the dataURL, we store the details with the logged in user and also send the same as the API’s response. Using the response of this API the client application can display the secret key and the QR code on the screen. The user will then scan the QR code using an app like google authenticator.

Note that we are storing the secret as tempSecret at the moment, that is because we do not want to enable two-factor auth for the user unless the user has verified by providing the token once.

This API takes in token as a body param. We use the verify method to check if the token is valid. We pass the tempSecret as the secret. If the token is valid, we set the secret key for the user thus enabling 2fa for the user.

We also need an API to disable 2fa, if in case the user does not wish to have it and also an API to get 2fa details, this is required to display the QRcode and secret key.

app.js

//setup two factor for logged in user//..

//before enabling totp based 2fa; it's important to verify so that we don't end up locking the user.//..

Now finally we need to add the login API. Here we will accept the OTP in req headers. This is how it will work.
First, the login API must check if the user has enabled two-factor authentication. If not then the system must only check for credentials and authenticate the user. If 2fa is enabled, we again first check if the credentials are valid, this would be out 1st of the two steps in 2fa. If they are valid then the system will return a 206 status, which means partial success and also ask for the users OTP, upon the next request when the user enters the OTP, the client must also pass the credentials( which would be stored in the cache). Now since the credentials are valid and also the OTP is passed, we need to check if the OTP is valid or not. We do this obviously by using the verify() function. If the OTP is also valid, the system must authenticate the user.

Vue App

The vue.js app will again be for demo purposes, it will not necessarily follow best practices of building a vue.js application. Here we will have three components/screens, let’s break them down first before we have a look at the complete file.

In our Login we are taking in email and password as input and logging in the user. Now, depending on if 2fa is enabled or not we redirect the user to the right screen. So if 2fa is enabled the user must be redirected to enter the OTP (which is our OTP component), else the user will be logged in to the system.

Here, in our OTP component we have a field to allow the user to enter the OTP. Note that, on the click of the submit button we are calling the login API, but along with the OTP in the header as x-otp we are also passing the users credentials (email, password) in the request body. If the API returns a positive response we must log in the user.

Run the Application

This should start our app on port 3000. Navigate to localhost:3000 in any browser. Click on go to Login link. You should see a login form with pre populated data.

Login

Since two-factor authentication is not yet enabled, the user will get logged in to the system. Click on the Setup button to initiate setup. This will call the POST /towfactor/setup API we made earlier.

On successful response you will be asked to scan the QR code using a mobile App like Google Authenticator, alternatively, you can also enter the secret key.

setup

Once you scan this QRcode, Google Authenticator will start generating Time-based One Time Passwords. To completely enable 2fa, enter the token and click confirm. This will call our POST /towfactor/verify API to verify and enable Two-factor authentication for that user.

On successful verification, 2fa will be enabled. The current setting will be displayed on the setup page.

Now let’s head back to the login page and try to log-in again. Click on Go-to Login link.

Now once you hit login, the system will ask you to enter the OTP. Enter the token displayed by your Google Authenticator app.

Token

Now only if you enter the correct OTP, will the system allow you to proceed to the setup page.

Code Base

Conclusion

Thus we have learned how to implement two-factor authentication in a Node.js application. Here, I have also presented an opinionated approach to implement Two-factor authentication in a Stateless application, would love to hear your take on it.

Although we learned two-factor implementation, in a real application things would be a bit different, like the users would be in database and session/tokens should be handled on successful login. But that is completely up to the application developer to implement and is out of the scope of this article.

I’m curious about the data you are exposing through the `app.get(‘/twofactor/setup’, …);` endpoint. Is this secure? Even if you are authenticated you shouldn’t be able to get at the secret key right? The temp, maybe, but I don’t see what purpose this serves if you are just using it to determine whether the user has 2fa set up in the ui.

The API exposes, the dataUrl which is required to generate the QR code (without which the user wont be able to set up his google authenticator app), the second imp thing is the secret which is a fallback to the dataurl.
Note. this api will only be available to the authenticated user to fetch his own setup.