Coding

How to Build SMS Multi-Factor Authentication (MFA) with Twilio and Node.js

YN
yNeedthis
Author
How to Build SMS Multi-Factor Authentication (MFA) with Twilio and Node.js

Multi-factor authentication (MFA) is a security approach that requires users to prove their identity in more than one way before gaining access to an account. Instead of relying only on a password (something you know), MFA adds a second step using something you have, like your mobile phone.

In an SMS-based MFA flow, after a user enters their correct password, the system sends a short, one-time code (called a One-Time Password (OTP)) to their phone via text message. This code is temporary and usually expires after a short period of time. The user must enter this code into the application to complete the login process.

This extra step helps protect accounts because even if someone steals a password, they would still need access to the user’s phone to receive the code.

In this tutorial, you’ll build this exact flow using Twilio Verify, Node.js, Express, React, bcrypt, and JSON Web Tokens (JWT).

What You’ll Build

By the end of this tutorial, you’ll have built an application that allows users to:

  • Register with an email, password, and phone number
  • Log in using their email and password
  • Receive a one-time password (OTP) via SMS
  • Verify the OTP using Twilio Verify
  • Receive a JWT after successful verification
  • Access protected API routes using JWT authentication

Prerequisites

Before getting started, you’ll need:

  • Node.js installed
  • npm installed
  • Basic knowledge of JavaScript or TypeScript
  • Basic knowledge of React
  • A Twilio account
  • A mobile phone capable of receiving SMS messages

Why Use Multi-Factor Authentication?

Passwords alone are no longer enough to protect user accounts. If a password is stolen through phishing, data breaches, or password reuse, an attacker can immediately gain access.

MFA reduces this risk by requiring a second verification step. Even if someone knows your password, they still need access to your phone to complete the login process.

This additional verification significantly improves account security while remaining simple for users.

How Twilio Verify Works

Rather than generating and managing OTP codes yourself, Twilio Verify handles the entire verification process.

The flow is straightforward:

  1. Your application sends Twilio a phone number.
  2. Twilio generates a secure one-time verification code.
  3. Twilio sends the code via SMS.
  4. The user enters the code into your application.
  5. Your server asks Twilio to verify the code.
  6. Twilio responds with either approved or pending.

Your application never creates, stores, or validates OTP codes. Twilio manages the entire OTP lifecycle, allowing your backend to remain simple and secure.

Why use Twilio Verify instead of building your own OTP system?

You could generate random codes yourself, store them in a database, manage expiration times, resend limits, and brute-force protection. Twilio Verify handles all of this securely for you, allowing you to focus on your application’s authentication logic.

Application Flow

The application follows this authentication flow:

SMS MFA Flow

Architecture

After the user successfully enters their email and password, the backend validates the credentials. If they’re correct, Twilio sends an OTP via SMS. Once Twilio confirms the OTP is valid, the backend generates a JWT, which the frontend uses to access protected routes.

Step-by-Step Flow

  • The user registers with an email, password, and phone number.
  • The user logs in with their email and password.
  • The backend validates the password.
  • Twilio sends an SMS verification code.
  • The user enters the OTP.
  • The backend verifies the OTP with Twilio.
  • If approved, the backend generates a JWT.
  • The JWT is used to access protected API endpoints.

Notice: The JWT is not generated immediately after the password is validated. It is only issued after Twilio confirms the OTP is correct. This ensures the user has successfully completed both authentication factors.

Backend Overview

The backend is built with Express and exposes four API endpoints.

EndpointDescription
POST /auth/registerRegisters a new user
POST /auth/loginValidates credentials and sends an OTP
POST /auth/verify-otpVerifies the OTP and returns a JWT
GET /protectedExample protected endpoint

1. Register User

Passwords should never be stored in plain text. Instead, they should be securely hashed using bcrypt before being saved.

app.post("/auth/register", async (req, res) => {
  const passwordHash = await bcrypt.hash(password, 10);

  users.push({
    id,
    email,
    passwordHash,
    phone,
  });

  res.json({
    message: "Registered successfully",
  });
});

2. Login

After validating the user’s password, the backend asks Twilio Verify to send a one-time password to the registered phone number.

app.post("/auth/login", async (req, res) => {
  const valid = await bcrypt.compare(password, user.passwordHash);

  if (!valid) {
    return res.status(401).json({
      error: "Invalid credentials",
    });
  }

  await twilioClient.verify.v2
    .services(VERIFY_SERVICE_SID)
    .verifications.create({
      to: user.phone,
      channel: "sms",
    });

  res.json({
    message: "OTP sent",
  });
});

3. Verify the OTP

The submitted code is sent to Twilio for verification. If approved, the backend generates a JWT for the authenticated user.

app.post("/auth/verify-otp", async (req, res) => {
  const result = await twilioClient.verify.v2
    .services(VERIFY_SERVICE_SID)
    .verificationChecks.create({
      to: user.phone,
      code,
    });

  if (result.status !== "approved") {
    return res.status(401).json({
      error: "Invalid or expired OTP",
    });
  }

  const token = jwt.sign(
    {
      userId: user.id,
      email,
    },
    JWT_SECRET,
    {
      expiresIn: "1h",
    }
  );

  res.json({
    token,
  });
});

4. Protected Route

Protected routes require a valid JWT in the Authorization header.

app.get("/protected", (req, res) => {
  const token = req.headers.authorization?.split(" ")[1];

  const decoded = jwt.verify(token, JWT_SECRET);

  res.json({
    message: "Welcome!",
    user: decoded,
  });
});

Environment Variables

Create a .env file with the following values:

TWILIO_ACCOUNT_SID=
TWILIO_AUTH_TOKEN=
TWILIO_VERIFY_SID=
JWT_SECRET=
VariableDescription
TWILIO_ACCOUNT_SIDYour Twilio Account SID
TWILIO_AUTH_TOKENYour Twilio Auth Token
TWILIO_VERIFY_SIDYour Twilio Verify Service SID (starts with VA)
JWT_SECRETA long, randomly generated secret used to sign JWTs

React Frontend

The React application consists of four pages connected with React Router.

Register

Collects the user’s email, password, and phone number before sending the data to:

POST /auth/register

Login

Authenticates the user’s email and password:

POST /auth/login

If successful, the application stores the user’s email in sessionStorage and redirects to the OTP page.

Verify OTP

The user enters the SMS verification code.

POST /auth/verify-otp

If the code is valid, the returned JWT is stored in localStorage.

Welcome

When the page loads, the frontend sends the JWT to:

GET /protected

If the token is valid, access is granted. Otherwise, the user is redirected to the login page.

Things to Know Before You Start

  • You need a Twilio account before you can build this project. After creating your account, you’ll use the Twilio Console to obtain your Account SID, Auth Token, and Verify Service SID (which starts with VA). Your Account SID and Auth Token are available on the Console dashboard. To create a Verify Service, navigate to Verify → Multi-channel Verification with Verify, click Show More, then select Start Building ( see images below). Once the service is created, copy the Service SID (VA…) into your .env file as TWILIO_VERIFY_SID.
Twilio Console
  • Twilio trial accounts can only send SMS messages to verified phone numbers. Add verified numbers in the Twilio Console under Verified Caller IDs.
  • This tutorial stores users in memory for simplicity. Restarting the server will erase all registered users. For production applications, use a database such as PostgreSQL, MySQL, or MongoDB.
  • Never allow users to change their phone number without verifying the new number first.
  • Always hash passwords using a secure algorithm such as bcrypt.
  • Set a reasonable expiration time for JWTs and never store sensitive information inside them.
  • In production, consider adding rate limiting to your login and OTP verification endpoints to help prevent brute-force attacks.

Conclusion

Congratulations! You’ve built a complete SMS-based multi-factor authentication system using Twilio Verify.

Instead of generating and validating OTP codes yourself, Twilio securely manages the verification process while your application focuses on authentication and authorization. After successful verification, the backend generates a JWT that allows users to access protected resources.

From here, you can extend this project by adding a database, refresh tokens, role-based authorization, password reset functionality, or additional verification methods such as email or authenticator apps. This is just a simple app to get exposure to MFA setup using Twilio.

The complete source code for this project is available on GitHub.

Thank you for reading

YN

yNeedthis

I’m Shareeza Hussain, a Software Engineer with 8+ years of experience building web applications across startups and emerging tech companies. I hold a Bachelor’s degree in Computer Science, postgraduate credentials in User Experience Design and Enterprise Software Development, and I’m currently pursuing a certification in Data Analytics for Behavioural Insights at the University of Waterloo. My work spans product-focused development, mentoring junior engineers, overseeing outsourced teams, and continuously testing new tools and technologies. This blog documents what I learn through hands-on experimentation — from coding and databases to AI-powered developer tools.

Leave a Reply

Your email address will not be published. Required fields are marked *