Skip to content

SMS Multi-Factor Authentication — Implementation and Security Considerations

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about SMS Multi. We cover key concepts, practical examples, and best practices to help you master this topic.

SMS-based MFA sends a one-time verification code to the user's phone via SMS, providing a second authentication factor that reaches users on any mobile device without requiring an authenticator app.

What You'll Learn

SMS MFA implementation, SMS gateway integration with Twilio, Code Generation and verification, phone number verification, Rate Limiting SMS delivery, and SIM swap risk mitigation.

Why It Matters

SMS MFA reaches the widest audience — every phone can receive SMS. However, it is less secure than TOTP due to SIM swap attacks. Understanding both the implementation and risks helps you make informed security decisions.

Real-World Use

AWS, PayPal, and Twitter offer SMS MFA as a fallback option. Durga Antivirus Pro uses SMS MFA for low-risk operations (password resets, notification preferences) while requiring TOTP for admin actions.

sequenceDiagram
    participant User as User
    participant API as Auth API
    participant SMS as SMS Gateway
    participant Phone as User Phone

    User->>API: Request MFA code (phone number)
    API->>API: Generate 6-digit code + 5-min expiry
    API->>SMS: Send SMS with code
    SMS->>Phone: "Your Durga code: 482913"
    User->>API: POST /mfa/verify (phone, code)
    API->>API: Check code validity + expiry
    API->>User: Token (MFA-verified)

Code Example: SMS Code Generation and Sending with Twilio

import random, time, os
from flask import Flask, request, jsonify
from twilio.rest import Client

app = Flask(__name__)

# Twilio configuration
TWILIO_ACCOUNT_SID = os.environ.get("TWILIO_ACCOUNT_SID")
TWILIO_AUTH_TOKEN = os.environ.get("TWILIO_AUTH_TOKEN")
TWILIO_PHONE_NUMBER = os.environ.get("TWILIO_PHONE_NUMBER")
twilio_client = Client(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN) if TWILIO_ACCOUNT_SID else None

# In-memory code store (use Redis in production)
sms_codes = {}

def generate_sms_code(length=6):
    """Generate a numeric SMS verification code."""
    return ''.join(str(random.randint(0, 9)) for _ in range(length))

def send_sms(to_phone, message):
    """Send SMS via Twilio."""
    if not twilio_client:
        print(f"[MOCK SMS] To: {to_phone}, Message: {message}")
        return True

    try:
        twilio_client.messages.create(
            body=message,
            from_=TWILIO_PHONE_NUMBER,
            to=to_phone
        )
        return True
    except Exception as e:
        print(f"Failed to send SMS: {e}")
        return False

@app.route("/api/mfa/sms/send", methods=["POST"])
def send_mfa_code():
    """Generate and send SMS MFA code."""
    phone = request.json.get("phone", "")
    user_id = request.json.get("user_id", "")

    # Validate phone number format
    if not phone.startswith("+") or len(phone) < 10:
        return jsonify({"error": "Invalid phone number"}), 400

    # Check rate limit
    rate_key = f"sms_rate:{phone}"
    if is_rate_limited(rate_key, 3, 300):  # 3 SMS per 5 minutes
        return jsonify({
            "error": "rate_limited",
            "message": "Too many SMS requests. Try again later."
        }), 429

    code = generate_sms_code()
    expiry = time.time() + 300  # 5 minutes

    # Store code
    sms_codes[phone] = {
        "code": code,
        "expiry": expiry,
        "user_id": user_id,
        "attempts": 0,
        "used": False
    }

    message = f"Your Durga Antivirus verification code: {code}. Valid for 5 minutes."
    sent = send_sms(phone, message)

    if not sent:
        return jsonify({"error": "Failed to send SMS"}), 500

    return jsonify({
        "message": "Verification code sent",
        "expires_in": 300,
        "phone_masked": phone[-4:].rjust(len(phone) - 3, "*")
    })

Code Example: SMS Code Verification

@app.route("/api/mfa/sms/verify", methods=["POST"])
def verify_sms_code():
    """Verify an SMS MFA code."""
    phone = request.json.get("phone", "")
    code = request.json.get("code", "")

    stored = sms_codes.get(phone)
    if not stored:
        return jsonify({"error": "No code requested"}), 400

    if stored["used"]:
        return jsonify({"error": "Code already used"}), 400

    if time.time() > stored["expiry"]:
        del sms_codes[phone]
        return jsonify({"error": "Code expired"}), 400

    # Rate limit verification attempts
    stored["attempts"] += 1
    if stored["attempts"] > 5:
        del sms_codes[phone]
        return jsonify({
            "error": "Too many attempts. Request a new code."
        }), 429

    if stored["code"] != code:
        return jsonify({"error": "Invalid code", "attempts_remaining": 5 - stored["attempts"]}), 401

    # Code verified — mark as used
    stored["used"] = True

    # Issue MFA-verified token
    access_token = jwt.encode({
        "sub": stored["user_id"],
        "mfa_method": "sms",
        "mfa_verified": True,
        "iat": datetime.datetime.utcnow(),
        "exp": datetime.datetime.utcnow() + datetime.timedelta(hours=1)
    }, SECRET, algorithm="HS256")

    return jsonify({
        "verified": True,
        "access_token": access_token,
        "method": "sms"
    })

Code Example: SIM Swap Risk Detection

class SIMSwapDetector:
    """Detect potential SIM swap attacks."""

    def __init__(self):
        # Track phone number changes per user
        self.phone_history = {}

    def check_sim_swap(self, user_id, phone):
        """Check if phone number was recently changed."""
        last_phone = self.phone_history.get(user_id)

        if last_phone and last_phone != phone:
            # Phone number changed — could be SIM swap
            time_since_change = self.get_time_since_last_auth(user_id)
            if time_since_change < 3600:  # Within last hour
                return {
                    "risk": "high",
                    "action": "require_additional_verification",
                    "message": "Phone number recently changed. Additional verification required."
                }

        self.phone_history[user_id] = phone
        return {"risk": "low", "action": "proceed"}

    def get_time_since_last_auth(self, user_id):
        """Get time since user's last successful authentication."""
        last_auth = last_auth_times.get(user_id)
        if not last_auth:
            return float('inf')
        return time.time() - last_auth

# Integration with login flow
@app.route("/api/auth/login", methods=["POST"])
def login_with_sms_mfa():
    username = request.json.get("username")
    password = request.json.get("password")

    if not validate_password(username, password):
        return jsonify({"error": "Invalid credentials"}), 401

    user = get_user(username)
    if user.get("mfa_enabled") and user.get("mfa_method") == "sms":
        sim_check = SIMSwapDetector().check_sim_swap(username, user["phone"])
        if sim_check["risk"] == "high":
            # Require email verification in addition to SMS
            send_email_verification(username)

        return jsonify({
            "mfa_required": True,
            "mfa_method": "sms",
            "phone_masked": user["phone"][-4:].rjust(len(user["phone"]) - 3, "*")
        }), 403

    return issue_token(username)

Common Mistakes

1. Not Rate-Limiting SMS Sending

Without rate limits, attackers can drain your SMS budget or spam users. Limit to 3-5 SMS per phone number per 5 minutes.

2. Exposing Whether a Phone Number Is Registered

Return the same response whether the phone exists or not. Otherwise, attackers can enumerate valid phone numbers.

3. Using SMS as the Only MFA Method

SMS is vulnerable to SIM swap attacks. Offer TOTP as a more secure alternative and use SMS only as a fallback.

4. Codes with Long Expiry

SMS codes should expire in 5 minutes or less. Longer Windows increase the risk of code interception.

5. No Code Invalidation on Use

One-time codes must be single-use. An attacker who intercepts the code before the user should not be able to use it.

Practice Questions

  1. How does SMS MFA differ from TOTP in terms of security?
  2. What is a SIM swap attack and how does it affect SMS MFA?
  3. Why should SMS sending be rate-limited?
  4. How long should an SMS verification code be valid?
  5. What additional verification can mitigate SIM swap risk?

Answers:

  1. SMS MFA sends codes over cellular networks, which can be intercepted via SS7 attacks or SIM swapping. TOTP generates codes offline on the device.
  2. An attacker convinces the mobile carrier to transfer the phone number to a SIM the attacker controls. All SMS codes then go to the attacker's device.
  3. Attackers can trigger SMS delivery to users as a nuisance or attempt to brute-force codes. Rate limits prevent abuse and control costs.
  4. 5 minutes maximum. The code is sent over an insecure channel and a longer window increases the window for interception.
  5. Track phone number changes, require email verification when the phone changes, and check the time since the last phone change before sending SMS codes.

Challenge: Build an SMS MFA system with Twilio integration, rate limiting, SIM swap detection, and a fallback to email verification when SIM swap risk is detected.

FAQ

Is SMS MFA better than no MFA?

Yes, SMS MFA is significantly better than password-only authentication. However, TOTP and hardware keys provide stronger security.

What SMS gateway should I use?

Twilio is the most popular with global reach. Vonage (Nexmo) and AWS SNS are alternatives. Choose based on regional coverage and pricing.

How do I handle international phone numbers?

Use the E.164 format (+1 for US, +44 for UK, etc.). Validate phone numbers with libraries like google-libphonenumber.

Can SMS MFA work for API authentication?

Indirectly. SMS verifies the user, who then receives API tokens. The SMS code itself is not used for API authentication.

What is the cost of SMS MFA?

Twilio charges ~$0.0079 per SMS in the US, more for international. For 100K authentications/month, expect $800-2000 in SMS costs.

Should I offer SMS as the default MFA method?

No. Prefer TOTP or push notifications. Offer SMS as a fallback for users who cannot install authenticator apps.

Mini Project

Build an SMS MFA service with Twilio integration, rate-limited code sending, verification with attempt tracking, SIM swap risk detection, and a mock SMS gateway for testing without real phone numbers.

What's Next

Now explore Passwordless Authentication with Magic Links for a password-free authentication experience.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro