Introduction
In today's digital space, user authentication and verification are crucial components of any application's security measure. One-time password (OTP) systems provide an extra layer of security by generating unique codes that can be used once and are valid for a short period.
This article will guide you through the process of building an OTP and verification feature using Redis and Node.js. Redis, a powerful in-memory data structure store, will be utilized to store and manage the OTPs efficiently.
Overview of OTP-based Authentication
In this section, we will go through an overview of OTP-based authentication and discuss its advantages over traditional password-based systems.
What is OTP-based Authentication?
One-time password (OTP) authentication is a mechanism that provides an additional layer of security during an authentication process.
Unlike traditional password-based authentication, which relies on fixed passwords, OTP-based authentications generate random and unique codes that are valid for a short period (typically a few minutes). Users are required to provide the OTP along with their username or email to gain access to the system.
Advantages of OTP-based Authentication:
OTP-based authentication offers several advantages over traditional password-based systems, a few of which are:
- Increased Security
- Mitigation of Password-related Risks
- Two-factor Authentication (2FA)
By implementing OTP-based authentication and verification in your applications, you can significantly enhance the security of user authentication while providing a seamless, user-friendly experience.
Let's move on to the next section, where we will set up the development environment for our project.
Setting Up the Development Environment
In this section, we will go through the process of setting up the development environment for building the OTP and verification feature with Redis and Node.js. We will first cover the installation of Node.js, setting up Redis, and creating a new Node.js project.
Setting Up Node.js:
To begin, you need to install Node.js on your system. Node.js is available for multiple operating systems and can be downloaded from the official Node.js website. Follow these steps to install Node.js:
- Visit the Node.js website using your web browser.
- Download the appropriate Node.js installer for your operating system (Windows, macOS, or Linux).
- Run the installer and follow the on-screen instructions to complete the installation process.
To verify that Node.js is installed correctly, open a command prompt or terminal and run the following command:
node --version
You should see the version number of Node.js printed in the console.
Setting Up Redis
Next, you need to set up Redis, the in-memory data structure store that will be used to store and manage the OTPs. Follow these steps to install and configure Redis:
- Visit the Redis website using your web browser.
- Download the Redis distribution package for your operating system. Since Redis does not run directly on Windows, you can either run it through WSL2 (Windows Subsystem for Linux) or by downloading this port of Redis for Windows here and installing it. In this tutorial, we will be using the latter.
- Open a command prompt or terminal and run the Redis server by executing the following command:
redis-cli
This will start the Redis server on the default port 6379
.
Creating a New Node.js Project
Now that you have Node.js and Redis all setup, let's create a new Node.js project for building the OTP and verification feature.
Follow these steps:
- Open a command prompt or terminal and create or navigate to the directory where you want to create your project.
mkdir tutorials && cd tutorials
- Run the following command to clone the template that I made for this project in my GitHub repository and after that, run it.
git clone https://github.com/DesmondSanctity/node-redis-otp.git && npm install
This will give you most of the project setup except the various logic for the OTP generation, Redis caching and data retrieval, which we will be implementing in the next section.
Generating and Caching OTPs with Redis
In this section, we will explore the implementation details of generating OTPs and caching the data with Redis. We will use a user signup/login system to demonstrate this concept. We would also use a Node.js client library redis
and otp-generator
to interact with the local Redis instance and generate unique OTPs respectively.
Connecting to the Local Redis Instance
To utilize the caching capabilities of Redis, set up and connect to the local Redis instance you installed on your machine. In the server.js
file, set up the connection as shown below and export it for use in other files.
...
// create a client connection
export const client = redis.createClient();
// on the connection
client.on("connect", () => console.log("Connected to Redis"));
await client.connect();
...
Generating OTP for User and Storing to Redis
Now that you have established a connection with your local Redis installation, you can test generating OTPs and storing them in Redis. In the userController.js
file where the user logic is, add a method to generate OTP and store OTP using the packages we installed earlier.
...
export async function register(req, res) {
try {
const { username, password, email, firstName, lastName } = req.body;
// Generate a random OTP using the otp-generator package
const otp = otpGenerator.generate(4, {
lowerCaseAlphabets: false,
upperCaseAlphabets: false,
specialChars: false
});
// check the existing user
const existUsername = new Promise((resolve, reject) => {
UserModel.findOne({ username: username }, function (err, user) {
if (err) reject(new Error(err))
if (user) reject({ error: "Please use unique username" });
resolve();
})
});
// check for existing email
const existEmail = new Promise((resolve, reject) => {
UserModel.findOne({ email: email }, function (err, email) {
if (err) reject(new Error(err))
if (email) reject({ error: "Please use unique Email" });
resolve();
})
});
await Promise.all([existUsername, existEmail])
.then(() => {
if (password) {
bcrypt.hash(password, 10)
.then(hashedPassword => {
const user = new UserModel({
username,
password: hashedPassword,
email,
firstName,
lastName
});
// Store the OTP in Redis, with the user's email as the key
client.set(email, otp);
const { password, ...responseUser } = user._doc;
// return save result as a response
user.save()
.then(result => res.status(201).send({
msg: "User Register Successfully",
OTP: otp, User: responseUser
}))
.catch(error => res.status(500).send({ error }))
}).catch(error => {
return res.status(500).send({
error: "Enable to hashed password"
})
})
}
}).catch(error => {
return res.status(500).send({ error })
})
} catch (error) {
return res.status(500).send(error);
}
}
...
From the above code, the otpGenerator
function from otp-generator
package was used to generate four unique integers for the OTP and to store them in the Redis client using client.set(key, value)
; where value
is the OTP we are storing and key
is a unique identifier we will use to identify each stored value. In this case, the email of the user will be used as the key
because it is unique per user.
You can check your local instance to see if the OTP is saved by querying Redis with the key/identifier. Run the command below to see the resulting OTP for a user with email example@gmail.com
redis-cli
GET example@gmail.com
Verifying OTP for User
So far, a unique OTP has been generated on user registration, now we need to verify the user by writing another block of code. The logic here is to check the OTP to be supplied to the code block with the one in the local Redis instance, if they match, the user will be verified. If there is no match, the user will be notified that the OTP is incorrect.
...
export async function verifyUser(req, res, next) {
try {
const { username, email, otp } = req.body;
// check the user existance
let exist = await UserModel.findOne({ username: username });
if (!exist) return res.status(404).send({ error: "Can't find User!" });
// Retrieve the stored OTP from Redis, using the user's email as the key
const storedOTP = await client.get(email);
if (storedOTP === otp) {
// If the OTPs match, delete the stored OTP from Redis
client.del(email);
// Update the user's isVerified property in the database
await UserModel.findOneAndUpdate({ username }, { isVerified: true });
// Send a success response
res.status(200).send('OTP verified successfully');
} else {
// If the OTPs do not match, send an error response
res.status(400).send('Invalid OTP');
}
next();
} catch (error) {
return res.status(500).send({ error });
}
}
...
As you can see from the code, the client.get(key)
method was used to get the stored OTP and to compare it with the one input by the user. If there is a match, the OTP will be deleted using client.del(key)
method to avoid any vulnerabilities and the user info isVerified
will be updated to true.
Detecting Missed Verification
Sometimes, a user might register successfully but will fail to complete OTP verification. In such a case, the database will not record them as verified users yet, any attempt by such a user to log in will initiate a response with a notification for them to complete OTP verification. Also, a new code will be generated for them to use. An example code is below:
...
export async function login(req, res) {
const { email, password } = req.body;
try {
UserModel.findOne({ email })
.then(user => {
bcrypt.compare(password, user.password)
.then(passwordCheck => {
if (!passwordCheck) return res.status(400).send({
error: "Don't have Password"
});
if (!user.isVerified) {
// Generate a random OTP using the otp-generator package
const otp = otpGenerator.generate(4, {
lowerCaseAlphabets: false,
upperCaseAlphabets: false,
specialChars: false
});
// Store the OTP in Redis, with the user's email as the key
client.set(email, otp);
return res.status(400).send({
error: "User is not verified, please finish verification using this OTP",
OTP: otp
})
}
// create jwt token
const token = jwt.sign({
userId: user._id,
username: user.username
}, process.env.JWT_SECRET, { expiresIn: "24h" });
return res.status(200).send({
msg: "Login Successful...!",
user: user,
token
});
})
.catch(error => {
return res.status(400).send({ error: "Password does not Match" })
})
})
.catch(error => {
return res.status(404).send({ error: "Username not Found" });
})
} catch (error) {
return res.status(500).send({ error });
}
}
...
In the code block above, after the login credentials has been checked and ascertained correct, the isVerified
parameter will also be checked to see if the user is verified. If they are not verified, a new OTP is generated for them with a notification to complete the verification using the verify
endpoint that was done initially. Once they are verified, they can successfully log in to the system.
This is a short video demonstrating how the whole code put together works.
Conclusion
Implementing OTP-based authentication and verification is an effective way to strengthen the security of your applications. By using Redis as a data store and Node.js as the backend, you can easily build a robust OTP system.
Throughout this article, we went through step-by-step instructions and code examples to guide you in building this feature. With the knowledge gained from this tutorial, you are now equipped with adequate knowledge to implement OTP-based authentication and verification in your own projects, ensuring a safer user experience.
Top comments (0)