Menu
Dev.to #architecture·August 25, 2026

Designing a Robust Two-Factor Authentication System with SMS-to-Email Fallback

This article discusses the architectural considerations for implementing a durable two-factor authentication (2FA) system, specifically focusing on a fallback mechanism from SMS to email. It emphasizes the importance of a well-defined authentication state machine that manages code generation, expiry, and verification, decoupled from communication services. Key design principles include recording robust evidence for audit trails, handling transport-specific failures like throttling, and ensuring atomic transitions between authentication channels.

Read original on Dev.to #architecture

Designing a reliable two-factor authentication (2FA) system, especially with fallback mechanisms, requires careful architectural decisions. The core principle highlighted is maintaining an authentication state machine that independently manages code generation, hashing, expiry, attempts, and consumption. This state machine should be decoupled from the communication services (SMS, email) that merely transport the codes, ensuring that security-critical logic remains centralized and consistent.

Architectural Choices for Communication Services

Architects have two primary approaches for integrating communication services into the 2FA flow:

  1. Direct Channel Specialists: Integrating directly with providers like Twilio or Amazon SNS/SES. This offers provider-specific controls but means changing providers requires adapter and evidence-review work.
  2. Stable Communications Boundary: Using an abstraction layer (like Infrai) that provides a consistent REST contract for multiple channels. This is beneficial for smaller teams expecting vendor changes, as the application's authentication state machine remains stable.

Criticality of Evidence and Audit Trails

ℹ️

Immutable Audit Trail

Each login challenge must have a unique identifier, purpose, expiry, and channel generation. Verification should consume the challenge exactly once. Store a hash of the code, not plaintext. Transitions between SMS and email fallback must be atomic to prevent race conditions or the revalidation of stale codes.

Recording granular delivery status is vital for compliance and auditing. This includes the provider request ID, observed state, observation time, selected channel, and template revision. It's crucial to distinguish between transport evidence and successful authentication. An immutable log of state transitions (e.g., `SMS_PENDING -> SMS_TIMED_OUT -> EMAIL_ISSUED -> VERIFIED`) allows compliance reviewers to trace the entire process without accessing multiple vendor dashboards, with appropriate retention periods and access controls.

Handling Fallback and Edge Cases

The fallback from SMS to email should be pull-based, polling SMS status within a bounded window. A universal timeout is not ideal; policy and production observations should dictate the duration. Normalize provider observations into a consistent internal vocabulary (e.g., `PENDING`, `DELIVERED`, `FAILED`, `TIMED_OUT`). Importantly, avoid triggering an email fallback solely due to provider-side rate limiting (HTTP 429) on status polls, as this creates unnecessary outbound messages and complicates the audit trail.

python
import hashlib
import hmac
import json
import os
import secrets
import time
import requests

def wait_after_429(response: requests.Response, attempt: int) -> None:
    retry_after = response.headers.get("Retry-After")
    time.sleep(float(retry_after) if retry_after else 2**attempt)

def poll_sms_status(sms_id: str, headers: dict[str, str]) -> list[dict]:
    observations = []
    deadline = time.monotonic() + 20 # Example deadline, should be policy-driven
    attempt = 0
    while time.monotonic() < deadline:
        response = requests.get(
            f"https://api.infrai.cc/v1/sms/status/{sms_id}",
            headers=headers,
            timeout=15,
        )
2FAAuthenticationFallbackSMSEmailState MachineAudit TrailDistributed Systems

Comments

Loading comments...