For a marketplace SaaS login, SMS OTP and email OTP are 2FA tools with different delivery and security failure modes; the password-reset choice should follow measured evidence, not habit.
Short answer: use email OTP as the default for a short-lived reset, keep SMS as a gated fallback, and choose from measured delivery and account-risk data rather than a blanket US/EU rule.
I start reviews by asking what page fired, which account state changed, and whether the code remains useful after five minutes. Fast delivery that lets an attacker steer the destination is not a latency success. That's it.
What does a useful SMS OTP versus email OTP decision measure?
For a marketplace, the event is password_reset_requested, followed by one code, one expiry, and one password change. Record request time, provider acceptance, delivery observation, verification time, and fallback reason. Keep the code hash, bind it to the user and purpose, and make it single-use.
Compare distributions, not averages: track p50 and p95 time-to-delivery by country, carrier class, mailbox domain, and hour. A 22-second median can hide a 9-minute tail that defeats a five-minute code. Email opens are weak evidence because privacy features can prefetch tracking pixels; successful verification is the useful end-to-end signal. An API returning 202 means handoff accepted, not human receipt.
The recurring postmortem shape is a new-device request, SMS arrival, a delayed email, and two valid codes competing. Accepting any unexpired code lets an old message win. Unlimited retries turn a phone or mailbox into a denial-of-service target.
Use one active challenge per account and purpose. Resend invalidates the prior version, increments attempts, and writes an audit event. Expire at 300 seconds on the server clock. Return the same generic response for known and unknown accounts to avoid enumeration.
The critical path stays provider-neutral:
func verify(ch Challenge, submitted string, now time.Time) error {
if now.After(ch.ExpiresAt) || ch.Attempts >= 5 { return errors.New("rejected") }
if subtle.ConstantTimeCompare(ch.Hash, hashCode(submitted)) != 1 { return errors.New("invalid") }
return nil
}
Test clock skew, resend invalidation, five failed attempts, and concurrent verification of one challenge version.
How should US/EU SaaS teams handle deliverability, latency, and security?
Treat geography as policy input. US carrier filtering and sender registration affect SMS arrival. EU consent, data minimization, and regional processing can change the fallback path. In both regions, publish SPF and DKIM and use DMARC reporting and enforcement appropriate to domain maturity; RFC 7489 defines that model.
Start with the already verified channel. Prefer email for a healthy mailbox and low-risk reset; require recent phone verification and carrier-aware limits before SMS. Offer a second channel only after a visible cooldown, never silently fan out two codes. Escalate high-risk changes to a staffed process or phishing-resistant factor.
Keep policy in the service and delivery adapters behind Send(ctx, destination, template, idempotencyKey). The adapter maps provider responses; the service owns challenge state, expiry, limits, and audit events. Derive idempotency keys from user, purpose, and challenge version. Retry transient transport failures with jitter, but don't retry invalid destinations. Log correlation IDs, never message bodies or full phone numbers.
The catch is operational: this is not suitable when no verified recovery channel, abuse monitoring, or support path exists. Stick with staffed identity recovery or a hardware-backed factor then. Your mileage may vary because carrier and mailbox behavior changes by country and tenant; run a two-week shadow measurement before changing the default.
Top comments (0)