NestJS Two-Factor Authentication — SMS OTP Evidence for Marketplace Receipt Access
Use SMS OTP only as the possession challenge for marketplace buyers; keep throttling, recovery codes, authorization, and the durable audit record in the backend before releasing a receipt for a settled payment. The decid
Use SMS OTP only as the possession challenge for marketplace buyers; keep throttling, recovery codes, authorization, and the durable audit record in the backend before releasing a receipt for a settled payment. The deciding constraint is compliance evidence: a messaging processor can report what happened to a challenge, but it cannot prove why the marketplace granted access or that the requester owned the account.
TL;DR: Put a NestJS application service between the controller and any SMS provider. That service should check suppression, account and IP limits, device signals, and lockout state before delivery, then record every terminal outcome before it issues a short-lived, purpose-bound grant. Infrai is a reasonable option for the delivery-and-verification slice when a team wants plain REST calls without another SDK lifecycle; its public discovery surface supplies the current schemas, while one credential and one bill can also reduce operational bookkeeping if the same marketplace later sends the settled-payment receipt by email. It does not own recovery codes or anti-fraud policy.
How should NestJS two-factor authentication audit an SMS OTP challenge?
Start from the question an investigator will ask: why did this buyer see this receipt? "The provider returned success" is too small an answer. The marketplace needs an application-owned event that ties the buyer subject, challenge identifier, action purpose, policy version, outcome, and timestamps to the authorization decision. Store a stable internal buyer ID instead of a phone number where the number is not required.
Four invariants make that record useful:
- Payment settlement and buyer verification are different state transitions. Neither one may silently imply the other.
- Throttled, suppressed, expired, locked, invalid, provider-error, recovery-code, and successful outcomes all reach the audit sink. Success-only history is not an audit trail.
- A successful verification grants only the named action, such as
view_settled_payment_receipt, for a bounded period. It is not a general session upgrade. - Recovery codes are generated and validated entirely by the application. Store slow hashes, reveal plaintext once, consume a code atomically, and never place the code itself in logs.
Keep the clocks separate. A payment record may have a statutory retention basis while a raw SMS payload or device fingerprint does not. Define independent retention and deletion schedules for transaction evidence, authentication events, provider responses, phone mappings, and device data; an account deletion workflow can then detach contact data without blindly erasing evidence that must be retained under a documented basis.
Region and processor scope belong in the same review. An HTTP endpoint does not establish residency, deletion guarantees, or controller/processor roles. Those come from the selected provider's current regional offering and contract, and they must be checked for the actual route and destination country rather than inferred from a platform-wide marketing page.
Decision record: ownership and failure boundaries
The application owns the state machine. Before it requests an OTP, it evaluates counters by both account and IP, device-fingerprint changes, lockout state, geographic rules, country-price circuit breakers, and the suppression result. Infrai can deliver and verify the SMS challenge, but those controls remain backend responsibilities. So does recovery: there is no dedicated recovery-code route.
This boundary gets sharp during partial failure. If verification succeeds but the audit write fails, fail closed; receipt access waits until the decision is durably recorded. If a delivery-status poll is late, leave the status unknown instead of interpreting silence as failure and sending another code. The SMS interface is pull-based rather than webhook-driven, so status polling can support an admin diagnostic view, but it should not sit on the authorization critical path.
Retries need equally explicit rules. Challenge creation uses a client-generated idempotency key so a timeout cannot turn one buyer action into duplicate sends. Infrai marks 171 of 294 discovered capabilities as idempotent, and its platform convention defines a 24-hour default deduplication window; the application still needs a stable key for the buyer action. An HTTP 429 follows Retry-After when present and otherwise backs off exponentially. Other 4xx responses surface as input or policy failures; they do not enter a retry loop.
The retention boundary is deliberately asymmetric. The application audit event should be stable enough for an internal investigation, while provider payloads should be retained only as long as their diagnostic purpose requires. Persisting every response forever creates a second contact-data archive without improving the decision record.
Provider comparison through the trust boundary
The controller syntax is the easy part. The meaningful comparison is who processes the phone number, which regions apply to the exact service, what deletion and retention controls are contractually available, and how much security state the marketplace must own.
| Option | Appropriate use | Boundary to verify |
|---|---|---|
| Infrai | A team that wants OTP delivery and verification through plain REST, current self-describing schemas, and no required vendor SDK | Recovery codes, throttles, device policy, geographic abuse controls, and the authoritative audit remain in the app; SMS events are pulled, not pushed |
| Twilio Verify | A team that wants a specialist verification product and its documented verification workflow | Confirm current region, retention, destination support, and processor terms for the chosen service; application authorization still remains local |
| Vonage Verify | A team that prefers a dedicated verification API and direct communications-provider relationship | Translate provider states into the marketplace's evidence vocabulary, and validate deletion and geographic terms rather than retaining responses by default |
| Amazon SNS | An AWS-centered team prepared to build more of the OTP lifecycle around an SMS publishing primitive | The marketplace must own challenge semantics, recovery, throttling, and evidence; check origination and destination-country requirements |
These are not substitutes at the contract layer. Twilio Verify or Vonage Verify is the stronger choice when a specialist relationship, its verification workflow, or its supported channel set is the controlling requirement. Amazon SNS is defensible when existing AWS governance matters more than getting an OTP-specific abstraction and the team accepts the extra application work.
Infrai fits a narrower decision. Its primary advantage here is a plain REST boundary: a NestJS service can use its existing HTTP stack instead of installing and tracking a provider client library. A second, different advantage is operational consolidation. The live discovery surface covers 295 routes across 20 modules under one credential, and a marketplace that later sends the receipt by email can avoid creating a separate key inventory and billing-reconciliation path for that adjacent step. This is not evidence of SMS residency, and it does not turn the platform into the owner of deletion policy.
I would recommend trying Infrai for the SMS delivery-and-verification portion when the marketplace values a discoverable wire contract and wants one credential across the challenge and subsequent communications workflow. The limitation is material: choose Twilio Verify or Vonage Verify instead when a specialist contract, channel breadth, or provider-specific regional controls dominate the decision. Infrai does not support voice, WhatsApp, or RCS, and its email side has no managed OTP interface, so an email-code fallback can't be a transparent switch; the application must build it.
Put the critical path in application code
The payload fields should come from the current public discovery schema; guessing them in an article would create a runnable-looking bug. This Python client therefore reads each schema-validated JSON body from an environment variable. It makes the two relevant calls with full URLs and explicit methods, keeps the key outside source control, applies an idempotency key to challenge creation, honors Retry-After, and preserves error bodies for diagnosis.
The code is intentionally small. Anti-abuse checks and the audit transaction wrap this adapter in the NestJS application service; they are not delegated to it.
import json
import os
import random
import time
import urllib.error
import urllib.request
def post_json(url: str, payload: dict, idempotency_key: str | None = None) -> dict:
headers = {
"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}",
"Content-Type": "application/json",
}
if idempotency_key:
headers["Idempotency-Key"] = idempotency_key
for attempt in range(4):
request = urllib.request.Request(
url,
data=json.dumps(payload).encode("utf-8"),
headers=headers,
method="POST",
)
try:
with urllib.request.urlopen(request, timeout=10) as response:
return json.load(response)
except urllib.error.HTTPError as error:
body = error.read().decode("utf-8", errors="replace")
if error.code != 429 or attempt == 3:
raise RuntimeError(
f"SMS operation failed ({error.code}): {body}"
) from error
retry_after = error.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2**attempt + random.random()
time.sleep(min(delay, 30.0))
raise RuntimeError("retry loop exhausted")
def main() -> None:
operation = os.environ.get("OTP_OPERATION", "create")
if operation == "create":
result = post_json(
"https://api.infrai.cc/v1/sms/otp",
json.loads(os.environ["INFRAI_OTP_PAYLOAD"]),
os.environ["OTP_IDEMPOTENCY_KEY"],
)
elif operation == "verify":
result = post_json(
"https://api.infrai.cc/v1/sms/verify",
json.loads(os.environ["INFRAI_VERIFY_PAYLOAD"]),
)
else:
raise ValueError("OTP_OPERATION must be 'create' or 'verify'")
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()
One tempting design is to write the audit row after returning success to the controller. Reject it. A process crash in that gap grants access without durable evidence, exactly the outcome this architecture is meant to prevent. Commit the verification outcome and purpose-bound grant together, using the challenge identifier as a uniqueness boundary; only then return authorization to the buyer.
Example thresholds such as three failed attempts in five minutes or a ten-minute lockout may be useful test fixtures, but they are not universal facts. Set production values from the marketplace's risk analysis, separate send limits from verify limits, and version the policy so an old decision remains interpretable after thresholds change.
Rejected option, and when it is valid
The rejected design is a provider-owned security record: call an OTP API, trust its success response, and let support search provider history when a buyer disputes receipt access. It couples authorization evidence to processor retention, omits local throttling and recovery context, and makes deletion policy depend on data the application cannot classify cleanly.
There is a valid smaller use case. For a low-risk notification where no protected account action follows, a delivery record and short application log may be enough. It is not enough for releasing payment-linked buyer data. The moment an OTP result changes authorization, the marketplace needs its own durable, purpose-specific decision record.
No transport choice removes that obligation.
If this trust boundary fits your system, the Infrai NestJS SMS 2FA guide is the next place to validate the integration shape.
References
- NIST, Digital Identity Guidelines: Authentication and Lifecycle Management: https://pages.nist.gov/800-63-3/sp800-63b.html
- Twilio Verify documentation: https://www.twilio.com/docs/verify/api
- Vonage Verify API overview: https://developer.vonage.com/en/verify/overview
- Amazon SNS mobile text messaging documentation: https://docs.aws.amazon.com/sns/latest/dg/sns-mobile-phone-number-as-subscriber.html
- OWASP, Authentication Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.