Implementing Secure Two-Factor Authentication with Twilio Verify
TwilioDevOpsTypeScript

Implementing Secure Two-Factor Authentication with Twilio Verify

Ihor (Harry) ChyshkalaIhor (Harry) Chyshkala

Why Two-Factor Authentication Matters

In today's digital landscape, password-only authentication is no longer sufficient to protect user accounts. Two-factor authentication (2FA) adds an essential second layer of security by requiring users to verify their identity through something they have (like a phone) in addition to something they know (their password).

Twilio Verify provides a reliable, scalable solution for implementing 2FA without the complexity of building and maintaining your own verification system. It handles SMS delivery, rate limiting, fraud detection, and verification code generation out of the box.

What is Twilio Verify?

Twilio Verify is a purpose-built API for user verification that supports multiple channels including SMS, voice calls, and email. Unlike sending verification codes through Twilio's standard messaging API, Verify offers several advantages:

• Built-in fraud detection and rate limiting
• Automatic code expiration and retry logic
• Support for multiple verification channels with fallback
• Compliance with carrier requirements for verification messages
• Detailed analytics and conversion metrics

Setting Up Twilio Verify

First, you'll need to create a Verify service in your Twilio console. This service will handle all verification requests for your application. Once created, you'll receive a Service SID that you'll use in your API calls.

Install the Twilio Node.js library:

npm install twilio

Basic Implementation

Here's how to implement a complete 2FA flow using Twilio Verify:

verify-service.js
const twilio = require('twilio');

const accountSid = process.env.TWILIO_ACCOUNT_SID;
const authToken = process.env.TWILIO_AUTH_TOKEN;
const verifySid = process.env.TWILIO_VERIFY_SERVICE_SID;

const client = twilio(accountSid, authToken);

// Step 1: Send verification code
async function sendVerificationCode(phoneNumber) {
  try {
    const verification = await client.verify.v2
      .services(verifySid)
      .verifications
      .create({
        to: phoneNumber,
        channel: 'sms' // or 'call' for voice
      });

    console.log('Verification sent:', verification.status);
    return { success: true, status: verification.status };
  } catch (error) {
    console.error('Error sending verification:', error);
    return { success: false, error: error.message };
  }
}

// Step 2: Verify the code entered by user
async function verifyCode(phoneNumber, code) {
  try {
    const verificationCheck = await client.verify.v2
      .services(verifySid)
      .verificationChecks
      .create({
        to: phoneNumber,
        code: code
      });

    console.log('Verification check:', verificationCheck.status);
    return {
      success: verificationCheck.status === 'approved',
      status: verificationCheck.status
    };
  } catch (error) {
    console.error('Error verifying code:', error);
    return { success: false, error: error.message };
  }
}

Express.js API Integration

Here's a practical example of integrating Twilio Verify into an Express.js application:

auth-routes.js
const express = require('express');
const router = express.Router();

// Send verification code endpoint
router.post('/auth/send-code', async (req, res) => {
  const { phoneNumber } = req.body;

  // Validate phone number format
  if (!phoneNumber || !/^\+[1-9]\d{1,14}$/.test(phoneNumber)) {
    return res.status(400).json({
      error: 'Invalid phone number. Use E.164 format (+1234567890)'
    });
  }

  const result = await sendVerificationCode(phoneNumber);

  if (result.success) {
    res.json({
      message: 'Verification code sent successfully',
      status: result.status
    });
  } else {
    res.status(500).json({ error: result.error });
  }
});

// Verify code endpoint
router.post('/auth/verify-code', async (req, res) => {
  const { phoneNumber, code } = req.body;

  if (!phoneNumber || !code) {
    return res.status(400).json({
      error: 'Phone number and verification code are required'
    });
  }

  const result = await verifyCode(phoneNumber, code);

  if (result.success) {
    // Code verified successfully - create session, JWT, etc.
    req.session.phoneVerified = phoneNumber;
    req.session.verifiedAt = new Date();

    res.json({
      message: 'Phone number verified successfully',
      verified: true
    });
  } else if (result.status === 'pending') {
    res.status(400).json({
      error: 'Invalid verification code',
      verified: false
    });
  } else {
    res.status(500).json({ error: result.error });
  }
});

module.exports = router;

Frontend Implementation

On the frontend, you'll typically implement a two-step flow:

auth-client.js
// Step 1: Request verification code
async function requestVerificationCode(phoneNumber) {
  const response = await fetch('/api/auth/send-code', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ phoneNumber })
  });

  const data = await response.json();

  if (response.ok) {
    // Show code input form
    showCodeInput();
  } else {
    showError(data.error);
  }
}

// Step 2: Verify the code
async function verifyCode(phoneNumber, code) {
  const response = await fetch('/api/auth/verify-code', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ phoneNumber, code })
  });

  const data = await response.json();

  if (response.ok && data.verified) {
    // Redirect to dashboard or complete login
    window.location.href = '/dashboard';
  } else {
    showError('Invalid verification code. Please try again.');
  }
}

Best Practices and Security Considerations

When implementing 2FA with Twilio Verify, keep these best practices in mind:

**Rate Limiting**: Implement additional rate limiting on your endpoints to prevent abuse. Twilio Verify has built-in protections, but you should add your own application-level limits.

**Phone Number Validation**: Always validate phone numbers in E.164 format (+[country code][number]) before sending them to Twilio.

**Secure Storage**: Never store verification codes in your database. Twilio Verify handles code storage and expiration for you.

**User Experience**: Provide clear instructions and error messages. Consider implementing voice fallback for users who don't receive SMS codes.

rate-limiting.js
// Rate limiting example with express-rate-limit
const rateLimit = require('express-rate-limit');

const verifyLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 3, // limit each IP to 3 requests per windowMs
  message: 'Too many verification attempts, please try again later.'
});

router.post('/auth/send-code', verifyLimiter, async (req, res) => {
  // ... verification code logic
});

Handling Multiple Channels

Twilio Verify supports multiple verification channels. You can implement a fallback mechanism for better reliability:

multi-channel.js
async function sendVerificationWithFallback(phoneNumber) {
  // Try SMS first
  let result = await sendVerificationCode(phoneNumber, 'sms');

  if (!result.success) {
    // If SMS fails, try voice
    console.log('SMS failed, attempting voice verification');
    result = await sendVerificationCode(phoneNumber, 'call');
  }

  return result;
}

async function sendVerificationCode(phoneNumber, channel = 'sms') {
  try {
    const verification = await client.verify.v2
      .services(verifySid)
      .verifications
      .create({
        to: phoneNumber,
        channel: channel,
        locale: 'en' // Optional: specify language
      });

    return { success: true, status: verification.status, channel };
  } catch (error) {
    return { success: false, error: error.message };
  }
}

Cost Considerations

Twilio Verify pricing is competitive with standard SMS pricing but includes additional features:

• Pay per verification attempt (not per code sent)
• Free retry logic if user requests code resend
• Included fraud detection
• No infrastructure maintenance costs

For most applications, the added security and reliability features make Verify more cost-effective than building a custom solution.

Monitoring and Analytics

Twilio provides detailed analytics for your Verify service through the console. Monitor these metrics:

• Conversion rate (codes sent vs. codes verified)
• Channel success rates (SMS vs. voice)
• Geographic performance
• Fraud detection alerts

Twilio Verify simplifies the implementation of secure two-factor authentication while providing enterprise-grade security features. By following the patterns outlined in this guide, you can quickly add 2FA to your application and significantly improve account security for your users.

The combination of ease of implementation, built-in security features, and reliable delivery makes Twilio Verify an excellent choice for any application requiring phone number verification or two-factor authentication.

Happy coding!

About the Author

Ihor (Harry) Chyshkala

Ihor (Harry) Chyshkala

Code Alchemist: Transmuting Ideas into Reality with JS & PHP. DevOps Wizard: Transforming Infrastructure into Cloud Gold | Orchestrating CI/CD Magic | Crafting Automation Elixirs