DEV Community

Cover image for Phone Auth with Express and Node.js
Faizan Pasha
Faizan Pasha

Posted on • Updated on

Phone Auth with Express and Node.js

A simple web tutorial to make a Phone Auth API with express using node.
Sign In user with Phone Number and OTP without Firebase or AWS Amplify.

I'm using Twilio in this example you could use any SMS provider.

Dependencies needed

  • Twilio

    call the API to send the OTP.

  • Crypto

To create a hash using sha265 for client identification.

  • jsonwebtoken

To send a JWT Token when the user is authenticated.

  • bodyParser

To parse the request body

Installing the dependencies

npm install Twilio crypto jsonwebtoken bodyParser
Enter fullscreen mode Exit fullscreen mode

Getting started

Import statements

let app = require('express')();
let crypto = require('crypto');
let jwt = require('jsonwebtoken');
let bodyParser = require('body-parser');
Enter fullscreen mode Exit fullscreen mode

A fake database for demo purpose, Make sure to connect it to a real database later

var users = [
    {
        id: 1,
        name: 'John Doe',
        number: '+1234567890',
    },
    {
        id: 2,
        name: 'Bob Williams',
        number: '1234567891',
    },
];
Enter fullscreen mode Exit fullscreen mode

Body parsing was bundled in express by default from v-4.16.0. since 2020 express has removed default body parsing. We have to do it manually now.

let jsonParser = bodyParser.json();
Enter fullscreen mode Exit fullscreen mode

Generating keys

Generate a JWT Key and Hash key to verify that the token sent by the client has not been tampered with.
There are a lot of ways to generate unique keys. I am using node to generate unique keys.

node
Enter fullscreen mode Exit fullscreen mode
require('crypto').randomBytes(64).toStrong('hex')
Enter fullscreen mode Exit fullscreen mode

This command will generate a random string. Copy it and paste it into the .env.

Run this command again for the second key and paste it into the .env.

HASH_KEY=<YOUR_HASH_KEY_HERE>
JWT_KEY=<YOUR_JWT_KEY_HERE>
Enter fullscreen mode Exit fullscreen mode

Setup Twilio

For the sake of this example, we’ll be using Twilio but the concepts and code examples remain true for all SMS providers out there.

Login to your Twilio console.

Screenshot 2022-09-22 at 5.13.24 PM.png

Copy the Account SID, Auth Token, and My Twilio phone number from the dashboard.
Paste it in .env

TWILIO_ACCOUNT_SID=<YOUR_ACCOUNT_SID_HERE>
TWILIO_AUTH_TOKEN=<YOUR_AUTH_TOKEN_HERE>
TWILIO_NUMBER=<YOUR_PHONE_NUMBER_HERE>
Enter fullscreen mode Exit fullscreen mode

Declare the .env variables

Declare the .env variables on top of the file to get the secret key from the .env variable

require('dotenv').config();
let hashKey = process.env.HASH_KEY;
let jwtKey = process.env.JWT_KEY;
var accountSid = process.env.TWILIO_ACCOUNT_SID;
var authToken = process.env.TWILIO_AUTH_TOKEN;
let twilioNumber = process.env.TWILIO_NUMBER;
Enter fullscreen mode Exit fullscreen mode

Declare the Twilio client your Account SID, Auth Token, and My Twilio phone number.

var client = require('twilio')(<Auccount SID>, <Auth Token>);
Enter fullscreen mode Exit fullscreen mode

Now we are ready to build the endpoints.

We have to build two endpoints

  1. Request OTP
  2. Verify OTP

1. Request OTP

Get the user number from the request body

let number = req.body.number;
Enter fullscreen mode Exit fullscreen mode

OTP generation logic
It will return 4 digit OTP, If you want to increase the length increase a 0 of 1000.

let otp = Math.floor(1000 + Math.random() * 9000);
Enter fullscreen mode Exit fullscreen mode

Create a secret hash to verify the client in 2. Verify OTP

    let ttl = 5 * 60 * 1000;
    let expires = Date.now() + ttl;
    let data = `${number}.${otp}.${expires}`;
    let hash = crypto.createHmac('sha256', hashKey).update(data).digest('hex');
    let secretHash = `${hash}.${expires}`;
Enter fullscreen mode Exit fullscreen mode

Check whether the user number is already registered

var user = users.find((user) => user.number === number);
Enter fullscreen mode Exit fullscreen mode

Handle the case if the user doesn't exist

    if (!user) {
        users.push({
            id: users.length + 1,
            name: '',
            number: number,
        });
    }
Enter fullscreen mode Exit fullscreen mode

Now the fun part. Sending the OTP through Twilio and secretHash through response.
Twilio might get some errors so it is an excellent practice to wrap it inside of try catch. call the client method from Twilio and send OTP through SMS.
The body property takes the message you want to send the client.
from property takes your Twilio phone number.
to property takes the client's number.

if the message is sent successfully, send the secretHast through response. If Twilio throughs error send "Error sending OTP" as response

try {
        client.messages
            .create({
                body: `Dear customer,\n Your OTP is ${otp}. PLEASE DO NOT SHARE THIS OTP WITH ANYONE.`,
                from: twilioNumber,
                to: number,
            })
            .then(() => {
                //Send the secret hash to the client
                res.json(secretHash);
            })
            .catch((err) => {
                //Handle the twilio error
                res.status(500).send('Error sending OTP');
            });
    } catch (err) {
        //Handle the error
        console.log(err);
    }
Enter fullscreen mode Exit fullscreen mode

2. Verify OTP

Get number, OTP, secretHash from the requesr body

    let { number, otp, secretHash } = req.body;
Enter fullscreen mode Exit fullscreen mode

Slice the hash and the expiry time from the secretHash sent to the client in 1. Request OTP

    let [hashValue, expires] = secretHash.split('.');
Enter fullscreen mode Exit fullscreen mode

Check if the has expired and handle the case.

    let now = Date.now();

    if (now > parseInt(expires))
        return res.json({ error: 'Timeout. Please try again' });
Enter fullscreen mode Exit fullscreen mode

Create a new hash using the user number, OTP, and the expiry time

    let data = `${number}.${otp}.${expires}`;
    let newCalculatedHash = crypto
        .createHmac('sha256', hashKey)
        .update(data)
        .digest('hex');
Enter fullscreen mode Exit fullscreen mode

Compare the new hash with the hash sent by the client. if they match then generate a JWT with the user information from the database and add an expiry period.

    if (newCalculatedHash === hashValue) {
        var user = users.find((user) => user.number === number);

        let payload = {
            number: number,
            name: user.name,
            id: user._id,
        };
        //Create a JWT token
        let token = jwt.sign(payload, jwtKey, { expiresIn: '1y' });

        //Send the token to the client
        return res.json(token);
    } else {
        //Handle the case when the hash does not match
        return res.json({ error: 'Invalid OTP. Please try again' });
    }
Enter fullscreen mode Exit fullscreen mode

Once complete, we’re all ready to preview the project. Run the following command to run a local development server.

node index.js
Enter fullscreen mode Exit fullscreen mode

Source code is available at Github.

Top comments (0)