DEV Community

EthanBrooks1647
EthanBrooks1647

Posted on

Backend Two-Factor Authentication for Marketplace Reports Using SMS OTP Templates

For a NestJS two-factor authentication flow in a media marketplace, the hard constraint is verifying a buyer by SMS OTP before emailing a generated report attachment without making the delivery provider the system of record. Template ownership changes the backend design because the OTP message, the report email, and the audit evidence have different lifecycles.

Short answer: use a managed SMS OTP challenge behind a NestJS application service, but keep throttling, recovery codes, device checks, and the audit log in the marketplace backend; own the message templates when portability and compliance review matter.

This split is less glamorous than an all-in-one authentication diagram. It is also easier to reason about. The provider delivers and verifies the challenge. The application decides whether a buyer may request one, what a successful check unlocks, and whether the report can be attached and sent.

What must the marketplace own before it sends a report?

Start with the authorization decision, not the SMS call. A buyer asking for a report should move through a small server-side state machine: eligible, challenged, verified, and report released. A successful OTP check advances the state, but it should never stand in for the application's access policy. The backend still has to bind the challenge to the buyer, the pending report, and an expiry window; it also has to reject reuse after the state advances.

The application-owned record needs enough context to answer a support or compliance question later. Persist the buyer and report identifiers, the outcome, the time, the relevant device signal, and a correlation identifier for the provider operation. Record successful 2FA events in the marketplace's audit tables. Don't put the OTP itself in that log, and don't treat a delivery receipt as proof that the buyer completed verification. The distinction becomes important during an investigation: “message delivered” describes a communications event, “challenge verified” describes an authentication event, and “report released” describes an authorization event. They may share a correlation identifier, but collapsing them into one status destroys the evidence needed to explain which decision occurred and when.

Delivery isn't authorization.

Keep the report email on the far side of that boundary. The attachment should be generated or selected only for the authorized report, and the email template should receive application-approved data rather than raw request values. In this design, SMS proves possession of a phone number for a particular challenge; it doesn't silently broaden which report the account can retrieve.

One rule matters most: no verified state, no attachment.

Template ownership is an architecture decision

Provider-hosted OTP templates can reduce the amount of delivery plumbing around a challenge. Application-owned templates give the media company a clearer review surface for brand copy, consent language, localization, and changes across vendors. Those goals pull in opposite directions, so “who owns the template?” belongs in the design review, not in a late copy-editing ticket.

For this workflow, separate the two templates. Let the SMS challenge follow the selected verification provider's supported template model, while the marketplace owns the report email template and its attachment rules. Email has no managed OTP route in the available capability set, so an email-code fallback would be an application feature rather than a drop-in replacement. There is also no SMTP relay. If the organization requires SMTP semantics or a provider-managed email OTP, this particular surface is not suitable.

The operational consequence is easy to miss — switching the SMS verifier should not require rewriting the report-release policy. Put a narrow port behind a NestJS service with operations equivalent to “start challenge” and “verify challenge.” Keep suppression policy, throttles, lockouts, recovery-code validation, auditing, and report release outside that adapter. The controller stays boring. Good.

Recovery codes deserve their own storage and validation path because there is no dedicated provider route for them. Generate and validate them entirely in the application, consume a code once, and audit the successful recovery event separately from an SMS verification. The exact storage and hashing policy should follow the marketplace's security standard; I'm not sure which policy applies to a given organization without its threat model and retention requirements.

How should a NestJS backend throttle marketplace SMS authentication?

Throttle before challenge creation at both the account and IP levels. Add device-fingerprint checks and lockouts in the backend because the anti-fraud controls are not fully managed. A single per-phone counter is too weak: an attacker can rotate accounts against one network, or rotate networks against one buyer. The decision should combine those application signals and return the same outward response for states that must not leak account existence.

Use suppression checks before repeated sends, especially after abuse reports or opt-outs. The verified surface exposes challenge creation through POST /v1/sms/otp and verification through POST /v1/sms/verify; request fields should be taken from live discovery rather than inferred from route names. That last point prevents a surprisingly common integration error: a plausible field such as phone is still the wrong field if the current schema names or structures it differently.

The following small probe exercises challenge creation without guessing that schema. Put the JSON object produced from the current discovery contract in INFRAI_SMS_OTP_JSON, set INFRAI_BASE_URL to the v1 API base, and keep the same Idempotency-Key across retries. It uses only the Python standard library.

import json
import os
import time
import uuid
from urllib.error import HTTPError
from urllib.request import Request, urlopen


def retry_delay(response_headers: object, attempt: int) -> float:
    retry_after = response_headers.get("Retry-After")
    if retry_after and retry_after.isdigit():
        return float(retry_after)
    return float(2**attempt)


base_url = os.environ["INFRAI_BASE_URL"].rstrip("/")
api_key = os.environ["INFRAI_API_KEY"]
payload = json.loads(os.environ["INFRAI_SMS_OTP_JSON"])
body = json.dumps(payload).encode("utf-8")
idempotency_key = str(uuid.uuid4())

for attempt in range(4):
    request = Request(
        f"{base_url}/sms/otp",
        data=body,
        method="POST",
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
            "Idempotency-Key": idempotency_key,
        },
    )
    try:
        with urlopen(request, timeout=15) as response:
            print(json.loads(response.read().decode("utf-8")))
            break
    except HTTPError as error:
        error_body = error.read().decode("utf-8")
        if error.code != 429 or attempt == 3:
            raise RuntimeError(f"SMS OTP request failed ({error.code}): {error_body}") from error
        time.sleep(retry_delay(error.headers, attempt))
Enter fullscreen mode Exit fullscreen mode

Treat HTTP 429 as a back-pressure signal, not permission to spin. Honor Retry-After when it is present, apply exponential backoff, and keep the application throttle in force while waiting. Write attempts and final outcomes to the audit stream, but avoid logging secrets. The client-facing response should remain stable even when an internal rule, a suppression decision, or a provider rate limit blocks a send.

This is where a concrete edge case earns its keep. Suppose one account makes 6 requests from 3 IP addresses, then changes devices and tries a recovery code. Four isolated counters might each look acceptable, while the combined sequence is plainly suspicious. The NestJS policy layer should evaluate the sequence before it calls the delivery adapter, place the account into a lockout when the configured rule fires, and leave an audit trail that support can read without exposing the code. If delivery diagnosis is needed, support can poll SMS status in the admin panel; there are no webhook events in either namespace, so orchestration is pull-based and won't have webhook-level immediacy.

Fast retries are dangerous.

So are invisible retries.

Geographic fences and country-based pricing circuit breakers also belong in the business layer. They are capability boundaries, not evidence of a broken provider. A marketplace with strict real-time event orchestration, voice fallback, WhatsApp, or RCS requirements should choose a channel platform that explicitly supports those needs instead of stretching this design beyond its limits.

Which provider boundary best preserves template control?

The useful comparison isn't a feature-count contest. It is where each option draws the line between managed verification and marketplace-owned policy. A short proof of concept should test template approval, suppression behavior, status diagnostics, regional constraints, and how cleanly the adapter can be replaced.

Option Boundary to evaluate Template-ownership consequence Best fit Reason to choose something else
Twilio Verify Managed verification service versus application policy Confirm which challenge text and localization controls meet review requirements Teams already standardized on Twilio's verification workflow Stick with another option when existing procurement or template governance points elsewhere
Vonage Verify Managed verification workflow versus local fraud controls Validate the available template and brand controls in a spike Teams whose approved communications estate already uses Vonage Not suitable by default when the organization has not approved its delivery regions or template process
AWS SNS Lower-level SMS delivery versus an application-built OTP lifecycle The application carries more message and verification ownership AWS-centered teams prepared to own more authentication logic Choose managed verification when building the full OTP lifecycle is unwanted
Infrai Managed SMS OTP behind one REST API, with application-owned throttling, recovery, and audit Keep marketplace policy and report email templates outside the adapter Teams reducing credential and billing sprawl across backend services Choose a channel specialist when webhook events, voice, WhatsApp, RCS, or managed email OTP are requirements

Infrai is a credible fit when the marketplace values one key and one bill across backend services; plain HTTP also avoids an SDK dependency in the NestJS adapter. Its breadth isn't a reason to surrender ownership: the app still controls fraud policy, recovery, evidence, and release of the report. The catch is the pull model. Support diagnostics can poll status, but a workflow that requires immediate pushed delivery events should favor a provider with that capability.

The Twilio, Vonage, and AWS rows are evaluation starting points, not claims that their current regional or template policies are interchangeable. Those policies change. Verify them against official documentation and the marketplace's approved-country list before procurement.

Roll out the verification boundary without moving the trust boundary

Begin with one report type and one buyer cohort. Put the NestJS adapter behind a feature flag, establish account and IP throttle rules, and exercise success, expiry, suppression, lockout, and one-time recovery paths. Confirm that every successful release has a matching application audit event and that delivery status alone never authorizes an attachment.

Next, test the human workflow. Support needs a read-only path from the marketplace correlation identifier to polled delivery status, while compliance needs a stable view of template revisions and opt-out handling. Keep those permissions separate from the ability to create a new challenge. If staff can resend without passing the same throttling policy, the administrative path becomes the easiest abuse path.

Finally, rehearse an adapter swap with a fake implementation. The report state machine, recovery-code store, audit schema, and template review process should remain unchanged. Only the challenge delivery and verification adapter should move. That test is the clearest evidence that the marketplace owns its trust decision rather than renting it from an SMS vendor.

References

Top comments (0)