Building a 2FA API Endpoint

Developing an API Endpoint to Generate and Email 2FA Pincodes: A Quick Guide

Irv Lloshi
6 min readNov 21, 2023

NOTE: After completing this code sample, if you want to deploy it into AWS, check out the Migrating Your API Endpoint to AWS article.

In this article, we’ll dive into the intricacies of building an API that not only generates 2-factor pincodes but also manages their lifecycle. This includes sending them to user-specified emails, verifying their authenticity, and allowing for their cancellation. Whether you’re building a new application or enhancing the security of an existing one, understanding how to implement such features is essential in today’s security-conscious world.

We’ll start by setting up our development environment with Node.js and MongoDB. Then, we’ll step through the process of creating the necessary API routes: one for generating the pincode, another for verifying it, and a third for cancelling it. Along the way, we’ll integrate nodemailer for email functionality and use Mongoose for seamless MongoDB interactions. Finally, we will leverage Postman for making requests to our API endpoint to test its functionality.

Prerequisites: Working knowledge of NodeJS, MongoDB, Postman, and Gmail permissions to send emails.

Getting Started

To get started, you will first need to initialize a new Node.js project:

npm init -y

Then install necessary packages:

npm install express mongoose nodemailer dotenv

Follow the folder and file structure highlighted below:

email-api-endpoint/

├── node_modules/

├── controllers/
│ ├── pincodeController.js

├── models/
│ ├── Pincode.js

├── routes/
│ ├── pincodeRoutes.js

├── utils/
│ ├── emailService.js

├── .env
├── package.json
├── package-lock.json
└── index.js

NOTE: Before we proceed to adding the code for these files, please make sure you have created a MongoDB account.

Source Code

Insert the provided code snippet into each JavaScript files.

index.js:

const express = require('express');
const mongoose = require('mongoose');
require('dotenv').config();

// Import routes
const pincodeRoutes = require('./routes/pincodeRoutes');

// Initialize express app
const app = express();

// Middlewares
app.use(express.json()); // for parsing application/json

// Use routes
app.use('/api/pincodes', pincodeRoutes);

// Connect to MongoDB
mongoose.connect(process.env.MONGO_URI, { useNewUrlParser: true, useUnifiedTopology: true })
.then(() => {
console.log('Connected to MongoDB');
// Start the server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
})
.catch(err => {
console.error('Could not connect to MongoDB', err);
});

// Error handling middleware
app.use((err, req, res, next) => {
res.status(500).send('Internal Server Error');
});

routes/pincodeRoutes.js:

const express = require('express');
const router = express.Router();
const pincodeController = require('../controllers/pincodeController');

// Route to generate a new pincode and send it via email
router.post('/generate', pincodeController.generatePincode);

// Route to verify a pincode
router.post('/verify', pincodeController.verifyPincode);

// Route to cancel a pincode
router.post('/cancel', pincodeController.cancelPincode);

module.exports = router;

controllers/pincodeController.js:

const Pincode = require('../models/Pincode');
const sendEmail = require('../utils/emailService');

const generatePincode = async (req, res) => {
try {
// Generate a random 4-digit pincode
const pin = Math.floor(1000 + Math.random() * 9000);

// Create a new pincode document
const newPincode = new Pincode({
pincode: pin,
email: req.body.email,
createdAt: new Date(),
status: 'active'
});

// Save the pincode in the database
await newPincode.save();

// Send the pincode via email
await sendEmail(req.body.email, `Your pincode is ${pin}`);

res.status(200).json({ message: 'Pincode generated and emailed successfully.' });
} catch (error) {
res.status(500).json({ message: 'Error generating pincode' });
}
};

const verifyPincode = async (req, res) => {
try {
const { email, pincode } = req.body;
const existingPincode = await Pincode.findOne({ email, pincode });

if (!existingPincode || existingPincode.status !== 'active') {
return res.status(400).json({ message: 'Invalid or expired pincode.' });
}

// Update pincode status to verified
existingPincode.status = 'verified';
await existingPincode.save();

res.status(200).json({ message: 'Pincode verified successfully.' });
} catch (error) {
res.status(500).json({ message: 'Error verifying pincode' });
}
};

const cancelPincode = async (req, res) => {
try {
const { email, pincode } = req.body;
const existingPincode = await Pincode.findOne({ email, pincode });

if (!existingPincode) {
return res.status(400).json({ message: 'Pincode not found.' });
}

// Update pincode status to cancelled
existingPincode.status = 'cancelled';
await existingPincode.save();

res.status(200).json({ message: 'Pincode cancelled successfully.' });
} catch (error) {
res.status(500).json({ message: 'Error cancelling pincode' });
}
};

module.exports = {
generatePincode,
verifyPincode,
cancelPincode
};

models/Pincode.js:

const mongoose = require('mongoose');

const pincodeSchema = new mongoose.Schema({
pincode: {
type: String,
required: true,
trim: true
},
email: {
type: String,
required: true,
trim: true,
lowercase: true
},
createdAt: {
type: Date,
default: Date.now,
expires: 300 // Pincode will automatically be deleted after 5 minutes (300 seconds)
},
status: {
type: String,
enum: ['active', 'verified', 'cancelled'],
default: 'active'
}
});

const Pincode = mongoose.model('Pincode', pincodeSchema);

module.exports = Pincode;

utils/emailService.js:

const nodemailer = require('nodemailer');
require('dotenv').config();

// Email transport configuration
const transporter = nodemailer.createTransport({
service: 'your_email_service', // Replace with your email service (e.g., Gmail, Outlook, etc.)
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS
}
});

const sendEmail = async (to, pin) => {
const htmlContent = `
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300&display=swap" rel="stylesheet" />
<title>Your Pincode</title>
<style>
body {
background-color: #f0f0f0;
height: 100vh;
font-family: "Roboto", sans-serif;
color: #333;
margin: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.container {
text-align: center;
background-color: #fff;
padding: 2em;
border-radius: 8px;
box-shadow: 0px 0px 15px rgba(0, 0, 0, 0.1);
}
h1 {
font-size: 2em;
margin-bottom: 1em;
}
.code {
background-color: #1e2a35;
color: #fff;
padding: 1em;
border-radius: 4px;
font-size: 2em;
margin: 1em 0;
display: inline-block;
cursor: pointer;
font-weight: bold;
}
footer {
position: fixed;
bottom: 0;
width: 100%;
text-align: center;
padding: 1em;
background-color: #1e2a35;
color: #fff;
font-size: 0.8em;
}
</style>
</head>
<body>
<div class="container">
<h1>Your One-time Verification Code</h1>
<p class="code" id="code" onclick="copyToClipboard()">${pin}</p>
<p>Please use this code to complete your verification process.</p>
</div>
<footer>
<p>&copy; 2023 Your Company. All rights reserved. <br /></p>
</footer>
<script>
function copyToClipboard() {
const textArea = document.createElement('textarea');
textArea.value = document.getElementById('code').textContent;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('Copy');
textArea.remove();
alert('Code copied to clipboard!');
}
</script>
</body>
</html>
`;
const mailOptions = {
from: process.env.EMAIL_USER, // Sender address
to: to, // List of receivers
subject: 'Your 2FA Pincode', // Subject line
html: htmlContent
};

try {
let info = await transporter.sendMail(mailOptions);
console.log('Email sent: ' + info.response);
return true;
} catch (error) {
console.error('Error sending email:', error);
return false;
}
};

module.exports = sendEmail;

.env:

MONGO_URI=mongodb://localhost:27017/yourDatabase
PORT=3000
EMAIL_USER=yourEmail@example.com
EMAIL_PASS=yourEmailPassword
EMAIL_SERVICE=Gmail

Run Your Application

npm start

You should then see Connected to MongoDB and Server is running on port 3000 messages in the console.

Testing

To begin testing with Postman, install and open the Postman application, then create a new request by clicking “New” and selecting “Request.” Input the URL of your API endpoint and select the relevant HTTP method (GET, POST, PUT, DELETE, etc.). For a POST request in your pincode API, for instance, choose POST as the request type, fill in the endpoint URL (localhost:3000/api/pincodes/generate), and add required data such as email and pincode in the request body. After setting up your request, hit “Send.” Postman will then execute the request to your server which will generate an email with the 2FA pincode provided.

Generate 2FA to Email API Call

Then you can try a similar approach to make API calls to the other routes:

localhost:3000/api/pincodes/verify

localhost:3000/api/pincodes/cancel

Make sure to add the respective properties in the body of each request as indicated in the pincodeController.js file.

Results

If everything worked correctly, you should have received an email such as the following:

2FA Pincode Received over Email

Conclusion

In conclusion, the creation of a 2-factor authentication (2FA) system using Node.js and MongoDB demonstrates the power and flexibility of these technologies in building robust and secure web applications. Throughout this guide, we have successfully walked through creating a MongoDB schema, configuring email services with Nodemailer, and defining the necessary API routes and controllers. The implementation of this system underscores the importance of security in modern web applications and showcases how developers can effectively integrate 2FA into their projects to enhance user security.

However, it’s crucial to note that while email-based 2FA adds a layer of security, it should ideally be used as a last resort. Email channels do not provide the same level of security as other 2FA methods like SMS, Silent Authentication, or Voice verification. This is largely due to the inherent vulnerabilities in email systems, such as susceptibility to phishing attacks and the potential for email accounts being compromised. In contrast, methods like SMS and Silent Authentication offer a more secure and direct way of verifying user identity, making them preferable choices for 2FA. As security threats continue to evolve, it’s important for developers and businesses to consider and implement the most secure and user-friendly authentication methods available.

NOTE: If you want to make this API publicly available, check out the Migrating Your API Endpoint to AWS article.

--

--

Irv Lloshi

Solutions Architect specializing in API, 2FA best practices & AI-driven communication workflows.