Two-factor authentication flow with Node and React
By: Alan Casagrande
Tags: Nodejs, Software Development, Two Factor Authentication, React, Cybersecurity

In this article we will write a simple React app to demonstrate a two-factor authentication flow, which will prompt for username and password and then a time-based generated password.

We will also generate a QR code containing the two-factor configuration that can be read by authenticator apps such as Google Authenticator, in order to generate one-time passwords.

If you are not familiar with two-factor authentication and how one-time passwords are generated, I recommend this excellent article.

Here we will focus on client and server interaction to successfully implement a two-factor authentication flow.

The full code is available at http://github.com/OnFrontiers/mfa-demo-node

Project structure

There are quite a few files to bootstrap our app and handle the different concerns. Hopefully I managed to keep their implementation to a minimum, so bear with me ;)

Let’s take a look at them:

- app

- components

- App.js

- Input.js

- Login.js

- api.js

- index.js

- server

server.js

storage.js

webpack.config.js

package.json

Here is where our app starts. We try to login with the session cookie and and then initialize the app:

Login request to the server:

The App component will control the authentication flow:

Our stock login form:

And a simple input component:

Here is our server implementation. We use a cookie session and serve our static files on the public folder.

The /login endpoint checks the user credentials if provided, otherwise it checks if a user has been restored from the session cookie:

The user state is saved in a .json file. This is not suited for production use, but it will do for our demo:

The dependencies:

There is a build configuration for the client bundle and another one for the server bundle. Both are processed with Babel:

Run the demo app

Install dependencies: npm install

Build the app: npm run build:dev

Start the server: npm run server:dev

It will be available at http://localhost:8080:

Setup two-factor authentication

There are two types of one-time password:

HOTP (HMAC-based one-time password) is an algorithm that uses HMAC with a secret to generate a one-time password based on a counter value.

TOTP (Time-based one-time password) provides the current time (typically in 30 seconds increments) as a counter value to generate a one-time password using HOTP.

The algorithm implementation is taken from the article mentioned above which directly follows the specs RFC 4226 (HOTP) and RFC 6238 (TOTP).

In this demonstration we are going to use TOTP.

We are going to need new dependencies:

npm install base32-encode base32-decode qrcode

We use the verifyOTP function to verify that the code provided by the user is correct. Note that we iterate over the window argument which defaults to 1, effectively checking time — 1 , time , and time + 1 .

The reason is that by the time the user send the verification code, the 30 seconds window for that code might have already passed and it would be considered invalid. Thus, for a better experience, we consider the last (and next) code valid as well. For better security, a window higher than 2 is not recommended.

If you choose not to use an authenticator app on your flow, you could send the code generated on generateTOTP to users via SMS or Email.

We need two new endpoints to setup two-factor authentication:

/mfa_qr_code : this endpoint generates a random secret for the authenticated user (if not yet generated), encodes the configuration URI as a QR code and serves it as a PNG image.

A typical configuration URI looks like this:

otpauth://totp/MfaDemo:alan?algorithm=SHA1&digits=6&issuer=MfaDemo&period=30&secret=5CZ4UNFL54LOGJ24ZIWUHBY

MfaDemo is the application issuing the configuration and alan is the application user. This will be displayed on the app authenticator UI.

Authenticator apps typically use SHA1 and issue 6 digit codes every 30 seconds by default. The secret value will make sure that the generated code is unique for that user.

/verify_otp : this endpoint verifies that the code sent by the user is correct, returning true if valid and false if invalid.

If valid, it also sets mfaEnabled user attribute to true . When MFA is enabled, we want to require a one-time password when the user is trying to log in. We will do that in a moment.

This is how our server code looks like after adding the two endpoints:

On our client code we add the API call to /verify_otp

and create a new form to prompt the one-time password. If MFA is not enabled yet, we show the QR code image, so the user can scan it on the authenticator app.

Our App component after adding MFA setup to the flow:

Require one-time password on log in

Once MFA is configured, we want to prompt the user for the verification code after log in. For that we need to make some adjustments in our server.

When validating the session on /login , we will check if MFA is enabled and whether is has been verified on the current session. If not, we return the 403 HTTP code, which in our app has the semantics that the MFA is required. If we receive a 403 response, we prompt the user for the verification code.

if (req.user.mfaEnabled && !req.session.mfaVerified) {

return res.status(403).end();

}

On /verify_otp , if the provided verification code is valid, we flag the session as mfaVerified :

req.session.mfaVerified = true;

And finally, to protect our future endpoints, we add this middleware:

// Routes beyond this point must have MFA verified if enabled

app.use(function (req, res, next) {

const user = req.user;

if (user && user.mfaEnabled && !req.session.mfaVerified) {

return res.status(403).end();

}

next();

});

Updated endpoints on server.js :

Our API call logic is updated to consider the HTTP 403 response:

The app initialization will pass the requireMfa prop to App component:

And the App component will use it to control the authentication flow:

Conclusion

It can be tricky to get an authentication flow right. Hopefully this demonstration will give you some insights to implement your own according to your needs.