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:
- Your application sends Twilio a phone number.
- Twilio generates a secure one-time verification code.
- Twilio sends the code via SMS.
- The user enters the code into your application.
- Your server asks Twilio to verify the code.
- 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:

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.
| Endpoint | Description |
|---|---|
POST /auth/register | Registers a new user |
POST /auth/login | Validates credentials and sends an OTP |
POST /auth/verify-otp | Verifies the OTP and returns a JWT |
GET /protected | Example 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=
| Variable | Description |
|---|---|
TWILIO_ACCOUNT_SID | Your Twilio Account SID |
TWILIO_AUTH_TOKEN | Your Twilio Auth Token |
TWILIO_VERIFY_SID | Your Twilio Verify Service SID (starts with VA) |
JWT_SECRET | A 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.envfile asTWILIO_VERIFY_SID.

- 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