Securing your web applications from unauthorized users is very important. You can increase the security with One Time Password (OTP) authentication in addition to the login credentials. An OTP is sent to the user’s email when they want to log in. This is a form of multi-factor authentication (MFA).
It is essential to learn how to apply Multi-Factor authentication in real-world applications, and this tutorial got you covered. This tutorial explains how to store users’ sessions in the Redis Store and send OTP using Nodemailer for authentication.
Goal
This tutorial helps you understand:
- What is Redis?
- How to utilize the Redis Store?
- How to set up an SMTP service using Nodemailer?
- How to protect routes?
Prerequisites
To follow this tutorial, make sure you have:
- Node Version 16.0+
- Basic knowledge of Node.js
- A Text Editor, preferably Visual Studio Code
As everything is in place, let’s jump right in.
Set Up the Server
In this section, you will learn how to start an Express server.
- Create a new folder and open it in a text editor. In this tutorial, this folder is named
Multi-Factor Auth
, but you can decide to call it anything you want. -
Open the terminal in the project’s path and run the following code to install the dependencies needed to start the Express server.
npm init --y npm i express npm i -D nodemon
The Express dependency will be used to set up the backend server and nodemon, installed as a devDependency, which will watch the file system and restart the server when a change is made.
- Create a new file named
server.js
in the project’s directory. For simplicity, this file will contain all the server code for this application. -
Add the following codes in the
server.js
file to set up an Express server to listen on port5000
.const express = require("express"); const app = express(); const PORT = 5000; app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); });
-
Open
package.json
and add the following code to start the server.{ "name": "multi-factor-auth", "version": "1.0.0", "description": "", "main": "server.js", "scripts": { "start": "node server.js", "dev": "nodemon server.js" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "express": "^4.18.1" }, "devDependencies": { "nodemon": "^2.0.16" } }
-
Start the server in development mode by running
npm run dev
in your terminal.
Set Up Nodemailer
Nodemailer is a Node.js module that allows you to send emails using an email service or an SMTP server.
To set up Nodemailer, you need to provide your email or SMTP service credentials.
Google mailing service (Gmail) will not support the use of third-party apps or devices like Nodemailer starting from May 30, 2022.
For this tutorial, we will use Microsoft’s Outlook Maling Service to send emails.
Set Up Outlook
- Navigate to Outlook’s sign-up page to register for an Outlook account if you don’t have one.
-
Add your phone number if you are prompted to do so.
This step can be skipped.
Sending Emails
After setting up your Outlook account, the next step is to use your Outlook account’s credential to send emails through Nodemailer.
-
Open your terminal and install the
nodejs-nodemailer-outlook
dependency.This module only needs your Outlook account credentials and message to send emails.
Now, run the code below in your terminal to install the
nodejs-nodemailer-outlook
dependency:npm i nodejs-nodemailer-outlook
-
Import the
nodejs-nodemailer-outlook
module intoserver.js
and add the configurations needed to send emails:const express = require("express"); const app = express(); app.use(express.json());//Enabling the body attribute. const PORT = 5000; app.post("/auth", (req, res) => {//Post request to 'http://localhost:3000' const nodeoutlook = require("nodejs-nodemailer-outlook");//Importing the module nodeoutlook.sendEmail({ auth: {//Outlook Account Credentials user: "<[email protected]>",//Email pass: "<yourOutlookPassword>",//Password }, from: "<[email protected]>", to: req.body.email,//Getting the email subject: "OTP", text: req.body.message,//Getting the Message onError: (e) => res.status(500).json({ message: `an error occurred ${e}` }), onSuccess: (i) => res.status(200).json({ message: i }), }); }); app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); });
-
To test the application, use an API testing application like Postman and make a POST request to
http://localhost:5000/auth
, passingemail
andmessage
to the body of the request.
Generate OTPs
In the previous section, you've successfully configured Nodemailer and used it to send emails. This section will show you how to generate a random number and how to add this random number to the message sent to the user.
You can generate this random number using:
Math.trunc(Math.random() * 1000000)
- A NodeJS module like UUID, shortid, nanoid, etc.
This tutorial uses the nanoid
module to generate random numbers.
-
In the terminal, install the
nanoid
module. This module will create a random set of characters as the OTP. Run the following command in your terminal:npm i [email protected] #Version compatible with Nodejs
-
After installing the
nanoid
module, import it intoserver.js
and use it to generate six random digits.const nanoid = require("nanoid");//Importing the naonid module const OTP = nanoid.customAlphabet("1234567890", 6)();//Generating
-
Add the OTP to the message sent to the user’s email.
const express = require("express"); const nanoid = require("nanoid"); const app = express(); app.use(express.json()); const PORT = 5000; const OTP = nanoid.customAlphabet("1234567890", 6)(); app.post("/auth", (req, res) => { const nodeoutlook = require("nodejs-nodemailer-outlook"); nodeoutlook.sendEmail({ auth: {//Outlook Account Credentials user: "<[email protected]>",//Email pass: "<yourOutlookPassword>",//Password }, from: "<[email protected]>", to: req.body.email,//Getting the email subject: "OTP", text: `Your One Time Password is ${OTP}`,//Adding the OTP onError: (e) => res.status(500).json({ message: `an error occurred ${e}` }), onSuccess: (i) => res.status(200).json({ message: i }), }); }); app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); });
-
Make a POST request to
http://localhost:5000/auth
, passing only the email address. -
Check the email address for which the OTP is sent.
Redis
Now that the OTP part of the authentication is settled, let’s set up the next part of the authentication. This section covers what Redis is and how to use the Redis Store to cache data.
What is Redis?
Redis, also known as Remote Dictionary Server, is an open-source (BSD licensed), in-memory data structure store. Developers use it as a database, cache, and message broker. It is a fast, lightweight database that structures data in key-value pairs.
Redis Cache
Storing and retrieving cached data from Redis is faster than traditional databases as it retrieves and stores data in the main memory (the RAM). The data can be users’ sessions or responses to requests.
The only downside is that data can be lost if the computer crashes.
Setting up Redis
Setting up Redis locally for Linux and Mac computers is simply straightforward.
Follow the official documentation for Linux and macOS
For Windows, you need to install Windows Subsystem for Linux Version 2 (WSL 2) and a Linux Distro on your local machine.
This tutorial uses Redis Cloud to set up the Redis database. Redis Cloud starts with Free a subscription fee that grants 30MB of storage. This is perfect for this simple application.
-
Create a new Redis Cloud account if you don’t have one.
- Click on Let’s start free and let Redis create a cloud database.
-
Click on Configurations and copy your Public endpoint. This endpoint will be used to set up the Redis application.
-
Still on the Configurations tab, copy your Default user password.
-
Run
npm i redis
in your terminal. Import theredis
dependency intoserver.js
, and set up theredisClient
using the Redis cloud endpoint.const redis = require("redis"); const redisConnect = async () => {//This function will be removed later in the tutorial try { const redisClient = redis.createClient({ url: "redis://redis-12345.c80.us-east-1-2.ec2.cloud.redislabs.com:12345", //This is an incorrect end point, insert your Public endpoint port: 12345,//The PORT is the last five digits in the url username: "default", password: "1234567890ABCDEFGHIJKLMNOPQRSTUV",//Input your Default_user_password }); await redisClient.connect();//Connecting to Redis Cloud await redisClient.set("Hello", "Hello from LoginRadius");//key=Hello, value=Hello from LoginRadius const Hello = await redisClient.get("Hello");//Getting the value using its key=Hello console.log(Hello); } catch (error) { if (//Running the function again when there is a network error error.message === "getaddrinfo ENOTFOUND redis-12345.c80.us-east-1-2.ec2.cloud.redislabs.com:12345" || error.message === "getaddrinfo EAI_AGAIN redis-12345.c80.us-east-1-2.ec2.cloud.redislabs.com" ) { return redisConnect(); } console.log(error.message); } };
-
Start up the server in your terminal and view the result.
Sessions
Now that Redis has been added to the application, let’s set up users’ sessions using Express Sessions.
Set Up Express Sessions
-
Install the
express-session
andconnect-redis
modules in your terminal.The
express-session
module allows you to create a session in the client’s browser. And theconnect-redis
module enables you to set Redis as the session store. Run the following command in your terminal.npm i express-session connect-redis
-
Import
express-session
andconnect-redis
intoserver.js
and configure the session store.const express = require("express"); const app = express(); const redis = require("redis"); const session = require("express-session");//Importing express session const RedisStore = require("connect-redis")(session);//Importing connect-redis and passing the session const redisClient = redis.createClient({ url: "redis://redis-12345.c80.us-east-1-2.ec2.cloud.redislabs.com:12345", //This is an incorrect end point, insert your Public endpoint port: 12345,//The PORT is the last five digits in the url username: "default", password: "1234567890ABCDEFGHIJKLMNOPQRSTUV",//Input your Default_user_password legacyMode: true, //Very important for Redis V4 }); redisClient .connect() .then(() => console.log("connected"))//Logs out connected on successful .catch((e) => console.log(e.message));//Logs out the error app.use( session({ secret: "LoginRadius",//session secret key resave: false, saveUninitialized: false, store: new RedisStore({ client: redisClient }),//Setting the store }) ); //The Rest of the code
Storing OTPs
Attributes stored in a session without a database are removed after the server refreshes. With the help of Redis cache, attributes in a session can be retrieved fast and saved even when the server restarts.
The user’s OTP should be saved in the session before sending it to the user’s email.
Open your server.js
file and add the code as follows.
The following code stores the OTP in the session, redirects when the email has been sent, and verifies the OTP provided with the one stored in the session.
app.post("/auth", (req, res) => {
let OTP = nanoid.customAlphabet("1234567890", 6)();//Changing the OTP
req.session.OTP = OTP;//Storing the OTP in the
const nodeoutlook = require("nodejs-nodemailer-outlook");
nnodeoutlook.sendEmail({
auth: {//Outlook Account Credentials
user: "<[email protected]>",//Email
pass: "<yourOutlookPassword>",//Password
},
from: "<[email protected]>",
to: req.body.email,//Getting the email
subject: "OTP",
text: `Your One Time Password is ${OTP}`,
onError: (e) => res.status(500).json({ message: `an error occurred ${e}, Please try again` }),
onSuccess: (i) => res.redirect("/verify"),//Redirecting the user to verify the token
});
});
app.get("/verify", (req, res) => {
res.send("Welcome to the verify route");//Welcome Message
console.log(req.session.OTP);//Logging out the OTP stored in the session.
});
app.post("/verify", (req, res) => {
const { OTP } = req.body;//Getting the user provided OTP
const OTPSession = req.session.OTP;//Getting the stored OTP
if (OTPSession) {//Checking if the OTP exist in the session
if (OTP === OTPSession) {//Verifying the similarity of the OTP
req.session.isAuth = true;//Setting auth to true
return res.status(200).json({ message: "OTP is correct" });
}
return res.status(401).json({ message: "Incorrect OTP" });
}
return res.status(404).json({ message: "OTP not found" });
});
Protecting Routes
Preventing unauthorized/unauthenticated users from accessing a specific route or a piece of important information is essential in various applications. In this section, you'll learn how to create a middleware that will authenticate the users accessing a specific route.
In this tutorial, the protected route is
/home
-
Create a
GET
route to/home
inserver.js
and add the middlewareauth
app.get("/home", auth , (req, res) => { res.send("You have Successfully been Authenticated"); });
-
Create the
auth
middleware function and verify if the user is authenticated.const auth = (req, res, next) => {//auth middleware function if (req.session.isAuth) {//Checking if `isAuth` = true return next(); } return res.redirect("/auth");//Redirecting the Login route }; app.get("/auth", (req, res) => {//Login route res.send("Login"); }); app.get("/home", auth, (req, res) => {//The Home route res.send("You have Successfully been Authenticated"); });
Logout
The logout route will also be protected using the auth
middleware to prevent requests from users who are not signed in.
app.post("/logout", auth, (req, res) => {
req.session.destroy();//Destroying the session
res.redirect("/auth");
});
Set Up the Frontend
It’s time to put the backend code into actual functionality. This tutorial makes use of Embedded JavaScript Template (EJS) as the frontend of this application.
Setting up EJS
-
Install the EJS Module
npm i ejs
-
Once the installation is complete, open your
server.js
and useejs
as the default view engineapp.set("view engine", "ejs")
-
In this application, there are only three routes. The login, verify, and home routes. You need to render an
ejs
file when accessing the routes.Modify the
GET
routes using the code as follows:app.get("/auth", (req, res) => { res.render("login");//Rendering the login ejs file }); app.get("/verify", (req, res) => { res.render("verify");Rendering the verify ejs file }); app.get("/home", auth, (req, res) => { res.render("home");Rendering the home ejs file });
-
Create a folder named
views
, and inside, create the variousejs
files.When the server is rendering an ejs file, it checks in the views folder for the file. So, this name is not optional.
Style these various routes as you like
Login Route
Add the following code to login.ejs
to set up the Login route.
<!DOCTYPE html>
<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">
<title>Login</title>
</head>
<body>
<h1>Login Page</h1>
<p>Please Sign in</p>
<form>
<input type="email" class="email"/><!-- Email input -->
<input type="submit" value="submit" class="submit"/>
</form>
<script>
const form = document.querySelector('form')//Getting the form
const email = document.querySelector('.email')//Getting the email
form.addEventListener('submit', async(e)=> {//Listening for a submit
e.preventDefault()
const data = { email: email.value }//Getting the email
await fetch("/auth", {//Making a POST request to http://localhost:5000/auth
method: "POST",
headers: {
"Content-Type": "application/json",
},
redirect: "follow",//Permiting redirects
body: JSON.stringify(data),//Sending the email
}).then(res => {
if(res.redirected) location.href = res.url//Redirecting the user on success
})
.catch((e) => {
email.value = ''
console.log(e.message);//Logging out error
});
})
</script>
</body>
</html>
Verify Route
Open verify.ejs
and set up the verify page as follows.
<!DOCTYPE html>
<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">
<title>Login</title>
</head>
<body>
<h1>Verify Page</h1>
<p class="message">Enter your OTP</p><!-- Message alert -->
<form>
<input type="number" class="OTP" /><!-- OTP input -->
<input type="submit" value="submit" class="submit" />
</form>
<script>
const form = document.querySelector('form')//Getting the form
const OTP = document.querySelector('.OTP')//Getting the OTP
const message = document.querySelector('.message')//Getting the message alert
form.addEventListener('submit', async (e) => {//Listening for a submit
e.preventDefault()
const data = { OTP: OTP.value }//Getting the OTP
await fetch("/verify", {//Making a POST request to http://localhost:5000/verify
method: "POST",
headers: {
"Content-Type": "application/json",
},
redirect: "follow",//Permitting redirects
body: JSON.stringify(data),//Sending the OTP
}).then(async(res) => {
if(!res.ok){//Checking for errors
const data = await res.json()
OTP.value = ''
return message.textContent = data.message//Alerting the error
}
if (res.redirected) location.href = res.url//Redirecting the user on success
}).catch((e) => {
OTP.value = ''
console.log(e.message)
});
})
</script>
</body>
</html>
Home Route
This is the route that is protected. The page can contain sensitive information that must not be accessible to unauthenticated users.
Open home.ejs
and configure the home route as follows.
<!DOCTYPE html>
<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">
<title>Login</title>
</head>
<body>
<h1>Home Page</h1>
<p>Hello authenticated user, This is a secret information for your eyes only😉</p>
Want to be unauthenticated, <button>Sign Out</button><!-- Sign Out button -->
<script>
const button = document.querySelector('button')//Getting the sign out button
button.addEventListener('click', async()=> {//Listening for a click
await fetch('/logout', {//Making a POST request to http://localhost:5000/logout
method: "POST",
headers: {
"Content-Type": "application/json",
},
redirect: "follow"//Permitting redirects
}).then((res)=> {
if (res.redirected) location.href = res.url //Redirecting the user on success
}).catch(e=> {
console.log(e.message)
})
})
</script>
</body>
</html>
Conclusion
This tutorial taught you how to set up Nodemailer, Redis Cloud, and Multi-Factor Authentication. With these, you've learned to secure your applications from unauthenticated users.
Do you think you can use this to build complex and scalable applications?
MFA with LoginRadius
You can also set up MFA with LoginRadius instead of setting up everything manually with Redis. You can sign up for a free account here.
Refer to this developer-friendly documentation to get started with your MFA setupdocs/guide/mfa/) for your web and mobile apps.
Resources
Click here to access the sample code used in this tutorial on GitHub