In todayâs digital landscape, secure user authentication is essential for protecting user data and application integrity. Email OTP (One-Time Password) verification enhances security by confirming user identity through time-bound codes sent to email.
- Builds a Node.js backend authentication server using industry-standard libraries
- Implements email-based OTP verification for an added security layer
- Follows a modern, scalable, and secure authentication approach suitable for real-world app.
Prerequisites
Before getting started, make sure you have the following:
- Basic understanding of Node.js and JavaScript
- Familiarity with a code editor (e.g., Visual Studio Code)
- Node.js and npm installed on your system
- Basic knowledge of REST APIs and MongoDB
Project Setup
1. Create a new project directory:
mkdir node-auth-otp
cd node-auth-otp
2. Initialize npm:
npm init -yThis will generate a package.json file to manage your project dependencies.
Dependencies
We'll utilize several popular npm packages:
- Express.js:Â Web framework for building Node.js applications
- Mongoose:Â ODM (Object Data Modeling) library for interacting with MongoDB
- Nodemailer:Â Email sending functionality
- bcrypt:Â Secure password hashing
- dotenv:Â Environment variable management
Install them using npm:
npm install express mongoose nodemailer bcrypt dotenvDatabase Setup (MongoDB)
1. Set up a MongoDB instance (locally or on a cloud provider).
2. Create a database (e.g., auth-db) for storing user information.
Environment Variables
Create a .env file in your project root and store sensitive details like your MongoDB connection string and email credentials:
MONGODB_URI=your_mongodb_connection_string
EMAIL_HOST=your_smtp_host
EMAIL_PORT=your_smtp_port
EMAIL_USER=your_email_username
EMAIL_PASS=your_email_password
Note: Never commit your .env file to version control.
Server Implementation (server.js)
Create the server.js file, where we configure the Node.js server, connect to the database, and define authentication-related API endpoints.
1. Import Required Modules
These dependencies provide essential functionality such as server creation, database interaction, email delivery, and security.
const express = require('express');
const mongoose = require('mongoose');
const nodemailer = require('nodemailer');
const bcrypt = require('bcrypt');
const dotenv = require('dotenv');
dotenv.config();
- express is used to build RESTful APIs.
- mongoose enables structured interaction with MongoDB.
- dotenv loads environment variables securely.
2. User Schema (MongoDB)
The user schema defines how authentication-related data is stored in MongoDB.
const userSchema = new mongoose.Schema({
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
otp: String,
otpExpire: Date
});
const User = mongoose.model('User', userSchema);
- Stores user credentials in a structured format.
- Ensures email uniqueness for each user.
- Includes OTP and expiration fields for verification.
3. Connect to MongoDB
This establishes a secure connection between the Node.js application and MongoDB.
mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true
})
.then(() => console.log('MongoDB connected'))
.catch(err => console.error(err));
- Uses environment variables for secure configuration.
- Enables modern MongoDB connection features.
- Logs connection status for debugging.
4. Initialize Express App
This sets up the Express application and enables JSON request handling.
const app = express();
app.use(express.json());
- Initializes the Express server and parses incoming JSON request bodies.
- Prepares the app for API route definitions.
5. Generate OTP
This function creates a random six-digit OTP for verification.
const generateOTP = () => {
return Math.floor(100000 + Math.random() * 900000);
};
- Generates a numeric 6-digit OTP
- Ensures randomness for security
- Can be replaced with a dedicated OTP library if needed
6. Send OTP via Email
This function sends the generated OTP to the userâs email address.
const sendOTP = async (email, otp) => {
const transporter = nodemailer.createTransport({
host: process.env.EMAIL_HOST,
port: process.env.EMAIL_PORT,
secure: false,
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS
}
});
const mailOptions = {
from: 'Your App <noreply@yourapp.com>',
to: email,
subject: 'OTP Verification',
text: `Your OTP is ${otp}. It will expire in 10 minutes.`
};
await transporter.sendMail(mailOptions);
};
- Uses Nodemailer with SMTP configuration
- Sends a time-bound OTP for security
- Supports easy customization of email content
7. User Registration with OTP
This endpoint registers a new user and initiates email-based OTP verification.
app.post('/register', async (req, res) => {
const { email, password } = req.body;
try {
const otp = generateOTP();
const hashedPassword = await bcrypt.hash(password, 10);
const newUser = new User({
email,
password: hashedPassword,
otp,
otpExpire: new Date(Date.now() + 10 * 60 * 1000)
});
await newUser.save();
await sendOTP(email, otp);
res.status(201).json({
message: 'Registration successful. Please verify your email using OTP.'
});
} catch (err) {
res.status(500).json({ message: 'Registration failed.' });
}
});
- Hashes passwords securely using bcrypt.
- Stores OTP with an expiration time.
- Sends OTP immediately after successful registration.
8. OTP Verification Endpoint (Placeholder)
This endpoint will validate the OTP entered by the user.
app.post('/verify-otp', async (req, res) => {
// Implement OTP validation logic here
res.json({ message: 'OTP verification endpoint' });
});
- Intended to verify submitted OTP values.
- Should handle expiration and mismatch cases.
- Clears OTP data after successful verification.
Expected Verification Flow:
- Find the user by email.
- Compare submitted OTP with stored OTP.
- Validate OTP expiration and remove it on success.
9. Start the Server
This launches the backend server and listens for incoming requests.
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
- Starts the application on the defined port.
- Uses environment variables for flexibility.
- Confirms server status via console logging.
Complete server.js File
Here is the complete server.js file including full implementation:
const express = require('express');
const mongoose = require('mongoose');
const nodemailer = require('nodemailer');
const bcrypt = require('bcrypt');
const dotenv = require('dotenv');
dotenv.config(); // Load environment variables
// User schema for MongoDB
const userSchema = new mongoose.Schema({
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
otp: String, // Field to store OTP
otpExpire: Date // Field to store OTP expiration time
});
const User = mongoose.model('User', userSchema);
// Connect to MongoDB
mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true
})
.then(() => console.log('MongoDB connected'))
.catch(err => console.error(err));
const app = express();
app.use(express.json()); // Parse incoming JSON data
// Function to generate random OTP (placeholder)
const generateOTP = () => {
// Implement your desired OTP generation logic here
// (e.g., using a library like otp-generator)
return Math.floor(100000 + Math.random() * 900000); // Example 6-digit OTP
};
// Function to send OTP via email
const sendOTP = async (email, otp) => {
const transporter = nodemailer.createTransport({
host: process.env.EMAIL_HOST,
port: process.env.EMAIL_PORT,
secure: false, // Adjust based on your SMTP server configuration
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS
}
});
const mailOptions = {
from: 'Your App Name <noreply@yourapp.com>', // Replace with your sender info
to: email,
subject: 'Your OTP for Node Auth App',
text: `Your OTP is ${otp}. It will expire in 10 minutes.` // Customize message
};
try {
await transporter.sendMail(mailOptions);
console.log(`OTP sent to ${email}`);
} catch (err) {
console.error('Error sending OTP:', err);
}
};
// Register a new user (with email OTP verification)
app.post('/register', async (req, res) => {
const { email, password } = req.body; // Extract email and password
// Validation (optional)
// You can add checks for email format and password strength
try {
// Generate OTP
const otp = generateOTP();
// Hash password
const saltRounds = 10; // Adjust saltRounds as needed
const hashedPassword = await bcrypt.hash(password, saltRounds);
// Create a new user
const newUser = new User({
email,
password: hashedPassword,
otp,
otpExpire: new Date(Date.now() + 10 * 60 * 1000) // Set OTP expiration (10 minutes)
});
await newUser.save(); // Save user to database
// Send OTP email
await sendOTP(email, otp);
res.status(201).json({ message: 'Registration successful! Please check your email for OTP.' });
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Error during registration.' });
}
});
// Placeholder for OTP verification route (implement logic here)
app.post('/verify-otp', async (req, res) => {
// ... (logic to verify OTP and handle success/failure)
res.json({ message: 'Placeholder for OTP verification' });
});
const port = process.env.PORT || 3000;
app.listen(port, () => console.log(`Server listening on port ${port}`));
Security & Implementation Considerations
These points highlight essential steps and best practices to make the authentication system reliable and production-ready.
- Replace placeholder logic with a secure OTP generation and reliable email delivery implementation.
- Implement the /verify-otp endpoint to validate OTPs and check expiration time.
- Add robust error handling and proper input validation throughout the codebase.
- Apply production-level security practices such as rate limiting, logging, and secure configurations.