DEV Community

Opuama Lucky
Opuama Lucky

Posted on

How Implement Two-Factor Authentication with Node.JS and otplib

How to Implement Two-Factor Authentication with Node.js and otplib

Have you ever been concerned that hackers might gain access to your internet accounts? With two-factor authentication (2FA), you can put those concerns to rest. 2FA is a secret weapon for adding an additional layer of protection.

This article will show you how to simply integrate 2FA in your Node.js applications with otplib, providing users with effective security and peace of mind. Prepare to increase your app's security and user trust!

Understanding Two-Factor Authentication

Two-factor authentication (2FA), also referred to as two-step verification or dual-factor authentication, is a strong security measure requiring users to provide two different authentication factors to confirm their identity.

When 2FA is activated, you'll be asked to enter an extra code sent to your mobile phone, a token generated by Google, or another designated source, along with your password during the sign-in process. This added step significantly boosts the security of your account, making it far more challenging for hackers to gain access, even if they've obtained your password.

Importance of 2FA

A strong password can assist in protecting your accounts and data security, but in the event of a data breach, you need an additional layer of protection. This is where 2FA comes in. Here are some compelling reasons why you need 2FA:

  • Safeguards Your Online Accounts: 2FA protects your online accounts from unauthorized access.

  • Adds an Extra Layer of Security: 2FA goes beyond just a username and password, making it much more difficult for hackers to breach your accounts.

  • Enhances Consumer Confidence: Implementing 2FA boosts consumer confidence in organizations and their offerings, fostering deeper customer relationships and a favorable brand image.

  • Offers Heightened Protection Against Evolving Threats: 2FA provides enhanced protection against evolving cyber threats, allowing you to proactively outmaneuver potential attackers.

  • Provides Peace of Mind: Knowing that your accounts have an additional security measure in place gives you peace of mind.

Now, let's move on to the development phase.

Setting Environment

Run the following command to create and change into a directory:



mkdir 2FA

cd 2FA


Enter fullscreen mode Exit fullscreen mode

Next:



npm init


Enter fullscreen mode Exit fullscreen mode

This command prompts you for several things, such as the name and version of your application.

Next:



npm install express body-parser otplib qrcode speakeasy


Enter fullscreen mode Exit fullscreen mode

This command installs Express and all the necessary dependencies to run this application, which are explained below:

  • express: Express is a framework for creating web servers and managing HTTP requests and responses.

  • body-parser: Body-parser is an Express middleware that parses incoming request bodies before passing them on to handlers, making them available via the req.body property. It is also used to decode JSON and URL-encoded data.

  • otplib: The OTP (One Time Password) Library allows you to generate and verify one-time passwords.

  • qrcode: QRCode is a package for creating QR codes. t enables you to generate QR codes that can be used to encode information such as URLs, text, or other data, which can be scanned by a QR code reader or smartphone camera.

  • speakeasy: Speakeasy is a library that generates and verifies one-time passwords. It also has features like creating backup codes and interacting with other authentication techniques.

User Registration

Now that all our dependencies are successfully installed, create a new folder public with the file index.html and add the following code:



<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Register</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <h1>2FA Authentication</h1>
    <h2>Register</h2>
    <form id="register-form">
      <input
        type="text"
        id="register-username"
        placeholder="Username"
        required
      />
      <input
        type="password"
        id="register-password"
        placeholder="Password"
        required
      />
      <button type="submit">Register</button>
    </form>
    <p class="top-p">Have an account? <a href="login.html">Login</a></p>
    <div id="message"></div>
    <div id="qr-code"></div>
    <script src="app.js"></script>
  </body>
</html>


Enter fullscreen mode Exit fullscreen mode

The code above creates a simple registration page for your app. Next, create a new file in the same public directory called login.html and add the following code:



<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <!doctype html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>2FA Authentication</title>
        <link rel="stylesheet" href="style.css" />
      </head>
      <body>
        <h1>2FA Authentication</h1>
        <h2>Login</h2>
        <div id="message"></div>
        <form id="login-form">
          <input
            type="text"
            id="login-username"
            placeholder="Username"
            required
          />
          <input
            type="password"
            id="login-password"
            placeholder="Password"
            required
          />
          <button type="submit">Login</button>
        </form>
        <script src="app.js"></script>
      </body>
    </html>
  </head>
</html>


Enter fullscreen mode Exit fullscreen mode

The code above creates the login page. One last page is needed to complete your frontend design. Create a new file in the same directory project/verify.html and add the code below:



<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <!doctype html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>2FA Authentication</title>
        <link rel="stylesheet" href="style.css" />
      </head>
      <body>
        <h1>2FA Authentication</h1>
        <h2>Verify 2FA</h2>
        <div id="message"></div>
        <form id="verify-form">
          <input
            type="text"
            id="verify-username"
            placeholder="Username"
            required
          />
          <input
            type="text"
            id="verify-token"
            placeholder="2FA Token"
            required
          />
          <button type="submit">Verify</button>
        </form>
        <script src="app.js"></script>
      </body>
    </html>
  </head>
</html>


Enter fullscreen mode Exit fullscreen mode

This code creates the page for verifying registered users. Now, enhance your frontend design by adding styles. Create a new file named style.css in your project directory and add the following code:



body {
  font-family: Arial, sans-serif;
  margin: 10rem 25rem;
  justify-content: center;
  text-align: center;
  background-color: snow;
}

form {
  margin-bottom: 10px;
}

input[type="text"],
input[type="password"] {
  border-radius: 10px;
  align-items: center;
  outline: none;
  display: block;
  margin: 10px 0;
  padding: 10px;
  width: 100%;
}

button[type="submit"] {
  position: absolute;
  border-radius: 10px;
  align-items: center;
  outline: none;
  margin: 5px 10px 10px -130px;
  padding: 10px;
  width: 20%;
}

#qr-code {
  margin-top: 10px;
}

p.top-p {
  margin-top: 55px;
}

p {
  margin-top: 5px;
}

#message {
  color: red;
}


Enter fullscreen mode Exit fullscreen mode

Enabling 2FA

To confirm that you have all dependencies installed, open your package.json file and you should see the exact image below:

json

If you have all of this in your JSON file, you're good to go. Now, create a new file named index.js and paste the following code into it:



const express = require("express");
const bodyParser = require("body-parser");
const otplib = require("otplib");
const qrcode = require("qrcode");
const path = require("path");
const crypto = require("crypto");

const app = express();
app.use(bodyParser.json());

const users = {};


Enter fullscreen mode Exit fullscreen mode

This code sets up the necessary libraries required to handle your 2FA and establishes a local storage to store user information when a user wants to register.

Generating Secret Key

Since you have all the dependencies needed to run this application, you can generate your secret key without any errors. To generate a secret key, add the following code to your index.js file.



app.post("/register", (req, res) => {
  const { username, password } = req.body;
  if (users[username]) {
    return res.status(400).send("User already exists");
  }

  const secret = otplib.authenticator.generateSecret();
  users[username] = { password, secret };
});


Enter fullscreen mode Exit fullscreen mode

The above code generates a secret key using the otplib library in the register endpoint.

Verifying 2FA

To verify your 2FA code, you'll need to extend the current setup to include a route for verifying the OTP (One-Time Password). Now, update your index.js file with the code below:



app.post("/register", (req, res) => {
  const { username, password } = req.body;
  if (users[username]) {
    return res.status(400).send("User already exists");
  }

  const secret = otplib.authenticator.generateSecret();
  users[username] = { password, secret };

  qrcode.toDataURL(
    otplib.authenticator.keyuri(username, "MyApp", secret),
    (err, imageUrl) => {
      if (err) {
        return res.status(500).send("Error generating QR code");
      }
      res.send({ secret, imageUrl });
    },
  );
});

app.post("/verify-2fa", (req, res) => {
  const { username, token } = req.body;
  const user = users[username];

  if (!user) {
    return res.status(400).send("User not found");
  }

  const isValid = otplib.authenticator.check(token, user.secret);
  if (!isValid) {
    return res.status(401).send("Invalid 2FA token");
  }

  res.send("2FA verification successful");
});


Enter fullscreen mode Exit fullscreen mode

The code above accomplishes several tasks: it creates a URI using the secret key, generates a QR code, and converts it into an image with the help of the URI. Also, the verify-2fa endpoint verifies the 2FA token provided by users. It extracts the username and token, retrieves user data, and utilizes otplib to verify the 2FA token.

Utilizing Backup Codes

The backup code serves as an alternative method to authenticate in case a user loses access to their primary 2FA device. These codes are provided to users as a last resort and should be kept confidential. To implement this, navigate to the index.js file and update the code with the following:



function generateBackupCodes() {
  const codes = [];
  for (let i = 0; i < 10; i++) {
    codes.push(crypto.randomBytes(4).toString("hex"));
  }
  return codes;
}

app.post("/register", async (req, res) => {
  const { username, password } = req.body;
  if (users[username]) {
    return res.status(400).send("User already exists");
  }

  const hashedPassword = password;
  const secret = otplib.authenticator.generateSecret();
  const backupCodes = generateBackupCodes();
  users[username] = { password: hashedPassword, secret, backupCodes };

  qrcode.toDataURL(
    otplib.authenticator.keyuri(username, "MyApp", secret),
    (err, imageUrl) => {
      if (err) {
        return res.status(500).send("Error generating QR code");
      }
      res.send({ secret, imageUrl, backupCodes });
    },
  );
});

app.post("/verify-2fa", (req, res) => {
  const { username, token } = req.body;
  const user = users[username];

  if (!user) {
    return res.status(400).send("User not found");
  }

  const isValid = otplib.authenticator.check(token, user.secret);
  if (!isValid) {
    const backupCodeIndex = user.backupCodes.indexOf(token);
    if (backupCodeIndex === -1) {
      return res.status(401).send("Invalid 2FA token or backup code");
    }

    user.backupCodes.splice(backupCodeIndex, 1);
  }

  res.send("2FA verification successful");
});


Enter fullscreen mode Exit fullscreen mode

The updated code generates backup codes and sends a response if user registration is successful.

Integrating User Authentication Flow

To integrate the user authentication workflow, create a file named app.js in your project directory and add the following code:



document.addEventListener("DOMContentLoaded", () => {
  const registerForm = document.getElementById("register-form");
  const loginForm = document.getElementById("login-form");
  const verifyForm = document.getElementById("verify-form");
  const messageDiv = document.getElementById("message");
  const qrCodeDiv = document.getElementById("qr-code");

  if (registerForm) {
    registerForm.addEventListener("submit", async (e) => {
      e.preventDefault();
      const username = document.getElementById("register-username").value;
      const password = document.getElementById("register-password").value;

      try {
        const response = await fetch("/register", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({ username, password }),
        });

        if (response.ok) {
          const data = await response.json();
          qrCodeDiv.innerHTML = `
            <img src="${data.imageUrl}" alt="QR Code">
            <p>Secret: ${data.secret}</p>`;
          messageDiv.innerHTML = `
            <p>Registration successful.</p>
            <p>Backup Codes (keep these safe):</p>
            <ul>${data.backupCodes.map((code) => `<li style="list-style-type: none;">${code}</li>`).join("")}</ul>`;
        } else {
          const error = await response.text();
          messageDiv.innerText = `Error: ${error}`;
        }
      } catch (error) {
        messageDiv.innerText = `Error: ${error.message}`;
      }
    });
  }

  if (loginForm) {
    loginForm.addEventListener("submit", async (e) => {
      e.preventDefault();
      const username = document.getElementById("login-username").value;
      const password = document.getElementById("login-password").value;

      try {
        const response = await fetch("/login", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({ username, password }),
        });

        if (response.ok) {
          messageDiv.innerText =
            "Login successful. Redirecting to 2FA verification...";
          setTimeout(() => {
            window.location.href = "verify.html";
          }, 1000);
        } else {
          const error = await response.text();
          messageDiv.innerText = `Error: ${error}`;
        }
      } catch (error) {
        messageDiv.innerText = `Error: ${error.message}`;
      }
    });
  }

  if (verifyForm) {
    verifyForm.addEventListener("submit", async (e) => {
      e.preventDefault();
      const username = document.getElementById("verify-username").value;
      const token = document.getElementById("verify-token").value;

      try {
        const response = await fetch("/verify-2fa", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({ username, token }),
        });

        if (response.ok) {
          messageDiv.innerText = "2FA verification successful";
        } else {
          const error = await response.text();
          messageDiv.innerText = `Error: ${error}`;
        }
      } catch (error) {
        messageDiv.innerText = `Error: ${error.message}`;
      }
    });
  }
});


Enter fullscreen mode Exit fullscreen mode

The provided code is designed to manage user interactions on the registration, login, and verification page of your application. It listens for form submissions and executes asynchronous requests to the backend server to handle user registration, login, and 2FA verification processes. Lastly, update your index.js file with the following code:



const express = require("express");
const bodyParser = require("body-parser");
const otplib = require("otplib");
const qrcode = require("qrcode");
const path = require("path");
const crypto = require("crypto");

const app = express();
app.use(bodyParser.json());

const users = {};

function generateBackupCodes() {
  const codes = [];
  for (let i = 0; i < 10; i++) {
    codes.push(crypto.randomBytes(4).toString("hex"));
  }
  return codes;
}

app.post("/register", async (req, res) => {
  const { username, password } = req.body;
  if (users[username]) {
    return res.status(400).send("User already exists");
  }

  const hashedPassword = password;
  const secret = otplib.authenticator.generateSecret();
  const backupCodes = generateBackupCodes();
  users[username] = { password: hashedPassword, secret, backupCodes };

  qrcode.toDataURL(
    otplib.authenticator.keyuri(username, "MyApp", secret),
    (err, imageUrl) => {
      if (err) {
        return res.status(500).send("Error generating QR code");
      }
      res.send({ secret, imageUrl, backupCodes });
    },
  );
});

app.post("/login", (req, res) => {
  const { username, password } = req.body;
  const user = users[username];

  if (!user || user.password !== password) {
    return res.status(401).send("Invalid username or password");
  }

  res.send("Login successful. Please verify with 2FA");
});

app.post("/verify-2fa", (req, res) => {
  const { username, token } = req.body;
  const user = users[username];

  if (!user) {
    return res.status(400).send("User not found");
  }

  const isValid = otplib.authenticator.check(token, user.secret);
  if (!isValid) {
    const backupCodeIndex = user.backupCodes.indexOf(token);
    if (backupCodeIndex === -1) {
      return res.status(401).send("Invalid 2FA token or backup code");
    }

    user.backupCodes.splice(backupCodeIndex, 1);
  }

  res.send("2FA verification successful");
});

app.use(express.static(path.join(__dirname, "public")));

app.listen(3000, () => {
  console.log("Server running on port 3000");
});


Enter fullscreen mode Exit fullscreen mode

The code above presents the complete index.js file in one block for easy reference. Also, a server path was added to access your application.

Testing

Before testing begins, ensure you have Google Authenticator installed on your device.

To start your server, run the following command:



node index.js


Enter fullscreen mode Exit fullscreen mode

serve

The response from the image shows that your server is running successfully. Next, enter http://localhost:3000 in your browser.

reg

The image above demonstrates a successful user registration. You can either scan the QR code or manually input the secret key into your Google Authenticator app. Also, you can use the backup code as a last-resort option.

auth2

The image displays the QR code token for tj24. Next, login and input your information as demonstrated in the screenshot below:

Image description

The clip above demonstrates a successful verification when using Google Authenticator to scan the QR code. Feel free to try using the setup key and backup code; you'll receive a positive response as well.

Note: The database operates on local storage.

Conclusion

Congratulations you have successfully integrated 2FA with Node.js and otplib. By applying the techniques described in this article, such as QR code-based token generation, secret keys, otplib, and backup codes. You can establish a system that is simple to deploy yet provides significant security benefits. Start building your 2FA system now and take the first step toward a safer digital environment!

Top comments (0)