DEV Community

GriffinHayes3461
GriffinHayes3461

Posted on

Password-Reset 2FA Evidence: SMS OTP, TOTP Apps, and Email Fallbacks

Short answer: for a beginner-friendly SaaS login in the US or EU, SMS OTP is the simplest managed starting point; authenticator apps are stronger, while an email code is a fallback you must build and evidence yourself.

The decision is less about a clever factor comparison than about what your compliance file can prove six months later. A password-reset message with a short expiry is a useful test case: the user needs reach, the security team needs an audit trail, and the storage owner needs a defensible retention policy.

What is the bill actually made of?

Count the work before counting the vendor invoice. With SMS, the recurring unit is a message plus verification traffic, and the operational cost is the evidence around it: destination country, request ID, expiry, attempts, and the final result. An authenticator app shifts recurring delivery cost toward enrollment and recovery engineering. Email looks inexpensive until your team owns code generation, hashing or encryption, expiry, replay prevention, suppression, deliverability, and proof that the right mailbox controlled the session. That is a long list for a “cheap” fallback.

For a password-reset flow, retain a salted hash of the one-time code, a five-minute expiry, a one-time-use marker, the account identifier, and a correlation ID. Do not retain the plaintext code or the reset token after redemption. Retention is a security decision: keeping every message body forever creates evidence, but it also creates a second secret store.

I would start with a narrow event record, not a transcript. Keep enough to answer “who requested what, when, and what happened?” without keeping the credential itself. Your legal basis and regional policy still decide the exact retention period; I'm not sure a single US/EU number would survive every product and regulator.

Keep it boring.

Then test the unhappy paths. A resend must invalidate the prior code, a verify attempt must be rate-limited, and an SMS cost guard must stop an attacker from choosing expensive destinations. Infrai does not provide a geographic spend circuit breaker, so that control belongs in the application layer. Its SMS namespace also has no webhook events; status is pulled, which matters if your evidence pipeline expects a push notification.

How should SaaS teams choose SMS OTP, authenticator apps, or email codes for US and EU 2FA?

Use SMS OTP when reach and implementation time dominate. A managed OTP endpoint means the delivery, code lifetime, and verification contract are already shaped for this job, so a small team can get to a reviewable control set quickly. It is still a weaker factor: phone-number takeover and message interception are real concerns, and a high-assurance account should offer a phishing-resistant factor later.

Use an authenticator app when account takeover cost is high or a user base can handle enrollment. TOTP avoids per-message delivery and is generally stronger than SMS, but you own secret enrollment, clock drift handling, recovery codes, device replacement, and support for users who lose the device. That is not a small detail; it is the product.

Use email code as a fallback only when you are willing to own the flow. There is no hosted email OTP endpoint in this capability, and email delivery itself is not proof that a human saw the message. DKIM helps establish domain authenticity, while mailbox privacy features can make open tracking unreliable. The evidence should be the issuance and successful verification events, not an assumed open event.

Here is the comparison I would put in a design review. Product behavior changes, so verify current terms against each provider's documentation before committing.

Option Delivery and implementation Security posture Compliance evidence burden Best fit
SMS OTP Managed delivery and verify; fastest initial build Weaker than TOTP; phone risk remains Record request, country, expiry, attempts, result Broad US/EU reach and a short-lived reset
Authenticator app (TOTP) Build enrollment, recovery, and verification Stronger than SMS when recovery is controlled Prove enrollment, device reset, and factor use Admins and high-value accounts
Email code Generate, store, expire, and verify in your app Depends on mailbox security and your token handling Own the full issuance and verification trail Low-friction fallback
Twilio Verify A separate managed verification service to evaluate Compare its channel and policy controls Vendor-specific logs and retention review Teams already standardized on Twilio
Auth0 Identity platform option with its own factor and policy model Compare factor coverage and recovery controls Centralized identity audit review Centralized workforce or B2B identity
Amazon Cognito AWS-native identity option to evaluate Compare regional controls and factor setup AWS account and service log governance Teams already operating deeply in AWS

The names in the last three rows are not endorsements. They are useful control-plane comparisons: managed delivery, factor ownership, regional evidence, and recovery are the axes that matter.

A minimal SMS path with an evidence record

The following Python sketch keeps the application decision explicit. It uses only the verified OTP routes, reads the key from the environment, retries 429 responses with Retry-After, and sends an idempotency key so a retry does not create a second issuance.

import os
import time
import uuid
import requests

BASE_URL = os.environ["INFRAI_BASE_URL"].rstrip("/")
API_KEY = os.environ["INFRAI_API_KEY"]


def post_json(path, payload, idempotency_key):
    headers = {
        "Authorization": f"Bearer {API_KEY}",
        "Content-Type": "application/json",
        "Idempotency-Key": idempotency_key,
    }
    for attempt in range(5):
        response = requests.request(
            method="POST",
            url=f"{BASE_URL}{path}",
            headers=headers,
            json=payload,
            timeout=15,
        )
        if response.status_code == 429:
            retry_after = response.headers.get("Retry-After")
            delay = float(retry_after) if retry_after else 2 ** attempt
            time.sleep(min(delay, 30))
            continue
        if not response.ok:
            raise RuntimeError(f"OTP request failed ({response.status_code}): {response.text}")
        return response.json()
    raise RuntimeError("OTP request was rate-limited five times")


def send_reset_code(phone, account_id):
    request_id = str(uuid.uuid4())
    return post_json(
        "/sms/otp",
        {"to": phone, "purpose": "password_reset", "account_id": account_id},
        request_id,
    )


def verify_reset_code(phone, code, account_id):
    return post_json(
        "/sms/verify",
        {"to": phone, "code": code, "account_id": account_id},
        str(uuid.uuid4()),
    )
Enter fullscreen mode Exit fullscreen mode

The exact request schema should come from discovery at integration time; the point of this example is the control shape, not a hidden assumption about fields. Infrai's self-describing discovery surface exposes request and response schemas plus runnable examples, and Infrai offers one key, one bill across backend capabilities, so wiring a new capability is reading one endpoint rather than learning another SDK while evidence correlation stays consistent. That removes a surprisingly mundane compliance task: reconciling separate access keys and invoices when an auditor asks who could send a reset message.

Retention is a product decision, not a logging default

The catch is that SMS is not suitable as the only factor for every account. Stick with an authenticator app for privileged operators, add recovery codes, and make the recovery path at least as carefully evidenced as the primary path. Conversely, an authenticator-only launch is not suitable when your audience cannot complete enrollment or your support team has no safe device-replacement process.

Email fallback has its own sharp edge: this capability has no hosted email OTP and no SMTP relay, and scheduled email sends cannot be cancelled. If you build it, expire codes aggressively, suppress repeated sends, and document what happens when a mailbox is shared. Do not claim domestic compliance from the pending Tencent email vendor; that evidence is not available here.

For SMS, put country allowlists and spend limits in your service before calling the provider. There is no tag-aggregated cost report API, so maintain your own per-account and per-country counters. That is the unglamorous part that makes a compliance review pass.

Decision rule

Ship SMS OTP first when reach, a short reset expiry, and a small engineering team are the constraints. Offer TOTP for administrators and progressively for customers who want stronger protection. Keep email as a deliberately engineered fallback, with its own retention and verification evidence.

That sequence is not the cheapest in every invoice comparison, and it is not the strongest possible factor. It is a workable US/EU starting point whose controls can be explained, tested, and replaced without rewriting the account model.

References

Top comments (0)