Authentication

How to make a scalable OTP service using NodeJs

OTP microservice

Divyansh Agarwal
Geek Culture

--

Photo by NeONBRAND on Unsplash

About OTP

One-time password (OTP) systems provide a mechanism for logging on to a network or service using a unique password that can only be used once.

Since the one-time passwords is valid for only a single-use, they are not vulnerable as static passwords and cannot be reused a second time by anyone, including unauthorized persons and thus avoiding the threat of pin code theft.

Problem in conventional OTP services:

In the conventional OTP service, the OTP is stored in a database along with the email or phone number for which it was used. Now, if the OTP service’s database is attacked by an attacker then the security of those applications which this type of OTP service serves might become at risk as the attacker could easily add an entry of OTP against any email or phone number in the database. Also, they can easily access the list of emails and phone numbers of many users thereby making the users at risk of attack.

Solution

To solve this problem the OTP can be stored in the database without can be email or phone number. We just need to store the OTP, expiration time and boolean field to mark the OTP verified or used. In this way, we can make the verification to be stateless by sending a unique and encrypted verification key when OTP is requested and send the OTP directly to the recipient. And when we need to verify the OTP we just need to have the OTP and verification key in the request body and the verification key will be decrypted and if it will be able to verify the OTP then it will return success otherwise if either OTP or verification key is altered then the service will return an error in the response. Thereby making the service secure and scalable.

Let’s Begin

Let’s first initialize the node project using npm init.

Final Folder Structure will look like this :

├───.env
├───.gitignore
├───app.js
├───package-lock.json
├───package.json
├───sequelize.js
├───middlewares
│ └───crypt.js
├───models
│ └───OTP.js
├───routes
│ ├───sendOTP_to_email.js
│ ├───sendOTP_to_phone.js
│ └───verifyOTP.js
└───templates
├───email
│ ├───forget.js
│ └───verification.js
└───sms
├───forget.js
└───verification.js

Install the following dependencies

npm i express cors dotenv morgan sequelize sequelize-cli pg helmet otp-generator nodemailer crypto aws-sdk nodemon

package.json

package.json

Let’s start with creating the app.js file.

In app.js we declare our express server and declare all the routes for different functions.

App.js

Models

So after initializing the node app let’s create models for our app.

OTP Model
This model will store the details like OTP, expiration time of OTP and a boolean verification field to mark OTP as used or verified.

OTP Model

Sequelize Connection file

In this file, we will connect the database with our project using the database credentials.

Sequelize Connection File

These dialect options are used to connect to a database service hosted on a cloud since they require SSL to be true.

dialectOptions: {
"ssl": {
"require": true,
"rejectUnauthorized": false
}
}
Environment File for setting Database

Routes

Send OTP to Email Route

In this route, we will take the email and type of the service request from any client using this OTP service and send the status and encrypted details in the response.

Send OTP to email Route

Now, let’s understand this code. First, we generate the OTP using otp-generator package. And then set the expiration time of OTP to 10 minutes after the current time. And then we create the OTP instance with OTP and expiration time in the OTP model in the database.

const otp = otpGenerator.generate(6, { alphabets: false, upperCase: false, specialChars: false });

const now = new Date();
const expiration_time = AddMinutesToTime(now,10);

const otp_instance = await OTP.create({
otp: otp,
expiration_time: expiration_time
});

Then we create an object having the OTP details along with its ID. And then encrypt the object using our encoder. This is one of the most important part of this service as this encrypted object will be used by us in the verification of the OTP after decryption. So any change in the encrypted string will result in an error in verification.

var details={ 
"timestamp": now,
"check": email,
"success": true,
"message":"OTP sent to user",
"otp_id": otp_instance.id
}
const encoded= await encode(JSON.stringify(details))

After encrypting the token we will check the type of message requested and select a template accordingly.

if(type){    
if(type=="VERIFICATION"){
const {message, subject_mail} = require('../Templates/Email/verification');
email_message=message(otp)
email_subject=subject_mail
}
else if(type=="FORGET"){
const {message, subject_mail} = require('../Templates/Email/forget');
email_message=message(otp)
email_subject=subject_mail
}
else{
const response={"Status":"Failure","Details":"Incorrect Type Provided"}
return res.status(400).send(response)
}
}

Then we use the nodemailer to send the email containing the OTP to the requested email. And if the mail is sent then the response is sent back to the client who sent the request.

You can refer to this article from freecodecamp to understand how we are using nodemailer.

Environment Settings for Nodemailer

Send OTP to Phone Route

In this route, we will take the phone number and type of the service request from any service using this OTP service and send the status and encrypted details in the response.

Send OTP to phone number Route

In this code, we followed the same steps as we followed in sending OTP to emails. The only change is in the part where we used AWS SNS from aws-sdk to send the SMS to the phone number requested.

You can refer to this documentation of AWS SDK to understand how we are using AWS SNS.

You can also use any other SMS service for this purpose like Twilio, Nexmo, etc.

Environment Settings for AWS SNS

Templates

Before moving to verify route let’s understand how templates are used to have a different message for a different type of request.

For the purpose of using this service as a microservice I created templates for different types of request like Forget Password, and Email or Phone Number Verification and we can add more templates for different functionalities. Although we can make different microservice sending the emails and SMS where we can make these templates but to complete the usage of the OTP service we will be including templates here only.

Email Templates

Email Template for Reset Password
Email Template for Email Verification
Email Template for 2FA

SMS Templates

SMS Template for Reset Password
SMS Template for Phone Number Verification
SMS Template for Login

In these templates, we export the message and use it when a particular type is requested by the client.

Verify Route

In this route, we will get three values in the request that is OTP, verification key and a check value having either email or number. After this, the client will receive Success if OTP Matches otherwise Bad Request.

OTP Verification Route

Now, let’s understand this code. First, we check if the encrypted object is altered or not if it’s altered then the client will receive a Bad Request (Error: 400) in response.

try{    
decoded = await decode(verification_key)
}
catch(err) {
const response={"Status":"Failure", "Details":"Bad Request"}
return res.status(400).send(response)
}

After this, we then check if the OTP which is being verified was sent to that email or phone number only.

var obj= JSON.parse(decoded)  
const check_obj = obj.check
if(check_obj!=check){
const response={"Status":"Failure", "Details": "OTP was not sent to this particular email or phone number"}
return res.status(400).send(response)
}

After an email or phone number verification, we then check if the OTP is available in the database or not. If it’s available then we then check if the OTP is already used or not. If it’s not used we then verify if OTP is expired or not. If OTP is not expired we then verify if OTP provided matches with the one in the database at the given ID. If all the conditions are true then the client receives a successful response that OTP Matches the OTP provided. If any of the condition fails client get a Bad Request (Error: 400) in response.

if(otp_instance!=null){    
if(otp_instance.verified!=true){
if (dates.compare(otp_instance.expiration_time, currentdate)==1{
if(otp===otp_instance.otp){
otp_instance.verified=true
otp_instance.save()
const response={"Status":"Success", "Details":"OTP Matched", "Check": check}
return res.status(200).send(response)
}
else{
const response={"Status":"Failure","Details":"OTP NOT Matched"}
return res.status(400).send(response)
}
}
else{
const response={"Status":"Failure","Details":"OTP Expired"}
return res.status(400).send(response)
}
}
else{
const response={"Status":"Failure","Details":"OTP Already Used"}
return res.status(400).send(response)
}
}
else{
const response={"Status":"Failure","Details":"Bad Request"}
return res.status(400).send(response)
}

Middleware

Encryption and Decryption Middleware

Now, we will understand how the encode and decode function was working to encrypt and decrypt the details object.

Encryption and Decryption Middleware

In the encode function when the string is passed, first, the key is created using password and salt ( empty string in our case). Then the cipher object is initialized for the AES-256 Algorithm using key and IV string (Initialization Vector). Then parts encrypted are concatenated and converted to base64 string to send it using JSON response.

async function encode(string) {    
var key = password_derive_bytes(password, '', 100, 32);
var cipher = crypto.createCipheriv('aes-256-cbc', key, ivstring);
var part1 = cipher.update(string, 'utf8');
var part2 = cipher.final();
const encrypted = Buffer.concat([part1, part2]).toString('base64');
return encrypted;
}

In the decode function when the encrypted string is passed, first the key is created using password and salt just like encode function. Then the decipher object is initialized for AES-256 Algorithm using key and IV string (Should be same in encode and decode). Then parts decrypted are concatenated and converted to object which is then used for verification.

async function decode(string) {    
var key = password_derive_bytes(password, '', 100, 32);
var decipher = crypto.createDecipheriv('aes-256-cbc', key, ivstring);
var decrypted = decipher.update(string, 'base64', 'utf8');
decrypted += decipher.final();
return decrypted;
}
Final Environment File

Instead of using a custom encode and decode function, you can also use CryptoJS for encryption and decryption as given in this solution on StackOverflow.

We can now easily implement an OTP microservice where we don’t need to store the email or phone number along with OTP in the database thus making it secure and scalable as we can create as many templates as we want for any type of request. Although it is not fully stateless, our state doesn’t have the emails or phone number stored in the database of our OTP service making the service scalable and secure.

Try it yourself

Now, that we have understood the working of the code we will see the working of API.

You can read the documentation of API endpoints and you can use them with the postman application or any other project.

Note: API endpoint for phone number will not work in this API as AWS credentials are not set

To give a preview of the OTP service I have also added 2 Factor Authentication for Login into my previous application, you can try it yourself here:

OTP Verification Screen in 2FA Application

Links

Link to my Previous Blog where I discussed how we can add manage login activity functionality to our app:

--

--

Divyansh Agarwal
Geek Culture

I am an Innovator with lots of ideas in my mind to improve the world for better! Know more about me at https://divyanshagarwal.info .