How Implement Two-Factor Authentication with Node.JS and otplib
Opuama Lucky
Posted on August 5, 2024
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
Next:
npm init
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
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>
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>
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>
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;
}
Enabling 2FA
To confirm that you have all dependencies installed, open your package.json
file and you should see the exact image below:
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 = {};
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 };
});
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");
});
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");
});
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}`;
}
});
}
});
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");
});
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
The response from the image shows that your server is running successfully. Next, enter http://localhost:3000
in your browser.
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.
The image displays the QR code token for tj24. Next, login and input your information as demonstrated in the screenshot below:
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!
Posted on August 5, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.