Support authenticator app verification for your Node.js app

This article introduces how to enhance the security of your Node.js app by integrating authenticator app verification such as Google Authenticator and Microsoft Authenticator.
Yijun
YijunDeveloper
October 25, 20238 min read
Support authenticator app verification for your Node.js app

In traditional applications, we typically use Email/Username/Phone as our identity identifiers. By combining these identity identifiers with corresponding passwords or verification codes, we can complete the process of authentication.

However, in scenarios with higher security requirements, relying solely on identity identifiers for authentication is not enough. This is because these identity identifiers and their corresponding verification information may be vulnerable to leaks.

In such cases, an additional layer of authentication is necessary to ensure that the entity being authenticated with the current identity identifier is the actual user. Common authentication methods include TOTP authentication using an authenticator app, biometric authentication, device authentication, and more.

In this article, we will explore how to integrate authenticator app verification into your Node.js app, enhancing the security of the authentication process for your users.

Introduction to TOTP

TOTP stands for Time-based one-time passwords. As the Wikipedia says, it is a computer algorithm that generates a one-time password (OTP) that uses the current time as a source of uniqueness.

With a shared TOTP secret key between the user's phone and the app server, the user's phone and the app server can generate the same TOTP code at the same point in time:

App ServerShared TOTP SecretUser PhoneApp ServerShared TOTP SecretUser PhoneRetrieve secretSecretGenerate TOTP code(211022) by the secret and current timeSend TOTP code(211022)Retrieve secretSecretGenerate TOTP code(211022) by the secret and current timeCompare the generated code with the received one (both 211022)Verification successful

As TOTP generation relies on time, it can be calculated offline. Additionally, TOTP produces a numerical string, making it simple and user-friendly. Therefore, verifying TOTP is commonly used as a means of 2-factor authentication.

When users use TOTP as a 2-factor authentication method, they often encounter the challenge of storing the TOTP secret and generating TOTP codes. This is where authenticator apps come in handy. We can use authenticator apps to store the TOTP secret and authenticator apps will automatically generate TOTP codes for you. When verification is needed, you just have to open your authenticator app, and you will get the TOTP code corresponding to the TOTP secret. Popular Authenticator apps include Google Authenticator and Microsoft Authenticator.

The process of implementing TOTP as two-factor authentication involves two steps:

  1. Binding a TOTP secret to the user.
  2. Verifying the user's TOTP code by the related TOTP secret.

The process of binding TOTP to a user is as follows:

App ServerUser PhoneAuthenticator AppApp ServerUser PhoneAuthenticator App(1) Sign in with an identifier(2) Identifier verified, but no TOTP secret is found from the user(3) Generate TOTP secret for the user and store locally(4) Send the TOTP secret to the User(5) Save the TOTP secret(6) Generate a TOTP code by the TOTP secret(7) Send the TOTP code to the Server(8) Generate a TOTP code from the local TOTP secret of the user(9) Compare the received TOTP code with the generate one(10) TOTP code verifed successfully, store the TOTP data for the user at user-level(11) TOTP verified, Sign in successfully

Once the user has bound TOTP, they can use it for verification. The process is as follows:

App ServerUser PhoneAuthenticator AppApp ServerUser PhoneAuthenticator App(1) Sign in with an identifier(2) Identifier verified, the user's TOTP secret is found(3) Ask the user for TOTP verification(4) Open the authenticator app(5) Generate a TOTP code by the user's TOTP secret(6) Send the TOTP code to the Server(7) Retrieve the verified user's TOTP secret and generate a TOTP code(8) Compare the received TOTP code with the generate one(9) TOTP code verified successfully(10) Sign in successfully

As illustrated in the diagram,on the user side, we use authenticator apps to manage the TOTP secret and generate TOTP codes. On the server side, we need to support generating a TOTP secret and validating the TOTP code sent by the user. In this article, we will use otpllib as an example to integrate TOTP-related functionalities on the server side.

Support TOTP for your Node.js app

Assuming your app is based on Express.js, and users log in through the /sign-in endpoint, the plan to support TOTP in the user login process is as follows:

  1. When the user is not bound to TOTP, send the TOTP secret to the user in the form of a QR code and prompt them to bind TOTP.
  2. When the user has already bound TOTP, prompt them to verify TOTP.

First, let's install the dependencies for the project: otplib and qrcode:

npm install otplib qrcode --save

Next, let's enhance our /sign-in endpoint.

import qrcode from 'qrcode';
import { authenticator } from 'otplib';

// Other codes ...

app.post('/sign-in', async (req, res, next) => {
  const { username, password } = req.body;
  const { totpSecret, userId } = await verifyIdentity({ username, password });

  // Cache identifier verified state
  req.inMemoSession.set('userId', userId);

  // Check if TOTP secret exist
  if (!totpSecret) {
    // Bind TOTP
    const secret = authenticator.generateSecret();
    const keyUri = authenticator.keyuri(username, 'DemoApp', secret);
    const secretQrCode = await qrcode.toDataURL(keyUri);

    // Cache pending TOTP secret for the user
    req.inMemoSession.set('pendingTotpSecret', secret);

    res.status(403).json({ error: 'missing_totp', secretQrCode });
    next();
    return;
  } else {
    // TOTP verification required
    res.status(403).json({ error: 'totp_verification_required' });
  }
  // ...
});

According to our implementation, when a user is not bound to TOTP, we will send a QR code to the frontend:

Bind TOTP

The user scans the QR code with the authenticator app, and the authenticator app will generate a TOTP code and store the related TOTP secret.

Google Authenticator

The user sends the obtained TOTP code back to the app server. If the code is successful verified, we can then bind this TOTP to the user.

So, let's implement a /verify-totp API to receive the TOTP code sent by the user:

app.post('/verify-totp', async (req, res, next) => {
  const { totpCode } = req.body;
  const userId = req.inMemoSession.get('userId');

  // Check pending TOTP secret to bind
  const pendingTotpSecret = req.inMemoSession.get('pendingTotpSecret');
  const totpVerified = authenticator.check(pendingTotpSecret, totpCode);
  if (!totpVerified) {
    res.status(400).json({ error: 'invalid TOTP code' });
    next();
    return;
  }

  // Verify successfully, start binding TOTP
  const user = DB.findUserById(userId);
  await user.update({ totpSecret: pendingTotpSecret });

  // Process user signing in
  // ...
});

This way, we have successfully bound TOTP for the user. Subsequently, when the user logs in, they only need to open the Authenticator App, send the verification code corresponding to the previously bound TOTP secret, and the authentication process will be completed.

Verify TOTP

In the /verify-totp API, we use the TOTP secret previously bound to the user to verify the TOTP code.

app.post('/verify-totp', async (req, res, next) => {
  const { totpCode } = req.body;
  const userId = req.inMemoSession.get('userId');

  // TOTP Binding ...
  // codes ...

  // TOTP verification
  const user = DB.findUserById(userId);
  const { totpSecret } = user;

  if (!totpSecret) {
    // Handle error
    // ...
    next();
    return;
  }

  const verified = authenticator.check(totpSecret, totpCode);
  if (!verified) {
    res.status(400).json({ error: 'invalid TOTP code' });
    next();
    return;
  }

  // Verify successfully
  // Process user signing in
  // ...
});

Summary

Based on this article, you should now be able to integrate authenticator app verification for your apps.

However, this is just a simple example. When your app is large or expected to become complex, integrating a new authentication method may incur significant costs.

The great news is: Logto, serving as a comprehensive identity authentication solution provider, offers multi-factor authentication (MFA) support, including authenticator app verification. By leveraging Logto, you can seamlessly integrate a secure and efficient user login process with MFA into your application in just a few minutes!