Dev.to Security πŸ” Cybersecurity πŸ‘ 0 πŸ“– 6 min read

Next.js Node.js 2FA Login SMS OTP: Evidence Boundaries for US and EU Delivery Records

The defensible design for a B2B SaaS compliance notice is an evidence ledger, not a delivery-status dashboard. Keep the login decision, message submission, transport observation, and user action as separate facts. A Next

The defensible design for a B2B SaaS compliance notice is an evidence ledger, not a delivery-status dashboard. Keep the login decision, message submission, transport observation, and user action as separate facts. A Next.js or Node.js handler can use the same small adapter for US and EU numbers, but it should never make authentication depend on a polling request completing.

This distinction is easy to lose because a single SMS OTP sits in the middle of two workflows. The user needs a short-lived challenge. The compliance team needs a record that explains what the system attempted and what the transport layer reported. Those are related records with different authority.

How should a Node.js 2FA login handle SMS OTP evidence?

Start with the reviewer’s question: can an engineer reconstruct the decision without opening a messaging console? The application should answer that question from its own events. A provider message reference can support the record; it cannot replace it.

For one challenge, record a stable challenge_id, the policy version, creation and expiry timestamps, the destination region needed for policy, and an ordered event sequence. Store a protected verifier rather than the OTP itself. Do not put the full phone number, the code, or message text into routine telemetry. A region label such as US or EU is useful for aggregate analysis; a unique destination label turns an audit field into an expensive metric dimension.

The event names should describe observations rather than conclusions: challenge_created, submission_accepted, transport_delivered, transport_failed, challenge_verified, or poll_deadline. submission_accepted means that the adapter accepted a request. transport_delivered means that the messaging path supplied a delivery signal. Neither event proves that a person read the notice or controlled the phone.

That is the first invariant.

Record Authority Retention purpose Boundary
Challenge and verification Authentication service Explain the access decision Atomic expiry and one-time use
Submission attempt Application and adapter Explain what was requested Idempotency and retry policy
Transport observation Messaging integration Reconcile delivery evidence Unknown states stay unknown
User action Application Explain code entry or acknowledgement Never infer it from delivery

This split also keeps retention decisions honest. Security decisions and terminal outcomes deserve a defined retention policy. Verbose polling traces do not automatically deserve the same treatment. I count both storage bytes and label cardinality before adding a dashboard field, because a field that is cheap in a log can become costly when copied into a time series.

Where should polling stop?

Polling is a reconciliation mechanism. It is not a second authentication factor and it is not a reason to hold the login request open.

Use a deadline and an interval with jitter. For an illustrative policy, a 10-minute challenge lifetime and a 30-second reconciliation window give the user room to complete a normal login while preventing a worker from waiting indefinitely on a carrier path. Those values belong in configuration with an owner and policy version; they are not protocol constants. The worker should persist every meaningful observation, stop on a terminal state, and write an explicit poll_deadline when the deadline wins.

The login handler can return a generic response after it has persisted the challenge and submission attempt. Verification then checks the protected challenge record, its expiry, and its one-time-use state. A delayed delivery update may improve the audit trail or user experience later, but it must not retroactively authorize a login.

The adapter boundary can be expressed without binding the application to a commercial API:

curl --request POST \
  --url https://messaging.example.internal \
  --header 'Content-Type: application/json' \
  --header 'Idempotency-Key: challenge-7f1c' \
  --data '{"channel":"sms","purpose":"login_otp","to":"+1XXXXXXXXXX","text":"Your sign-in code expires soon."}'

The returned message reference belongs in the attempt record. It does not belong in a high-cardinality metric label. A small counter grouped by channel, outcome, region, and policy version is generally more useful for operations than a unique series for every message.

Which failures belong to which owner?

The failure taxonomy should be decided before the code is deployed. A rejected destination is a delivery outcome. An incorrect code is an authentication outcome. A worker restart is a reconciliation failure. A user who requests another code triggers a new attempt that must remain linked to the original challenge or explicitly create a new challenge according to policy.

For US and EU traffic, normalize the destination at the integration boundary and retain only the regional fact required for policy and reporting. Test invalid numbers, unsupported destinations, delayed observations, duplicate callbacks, replayed callbacks, and worker restarts. Regional sender requirements and routing behavior can change, so configuration needs an owner and a review path rather than a hidden conditional in a request handler.

An unknown status is a valid state. Converting β€œno callback observed” into β€œfailed” creates a claim the system did not establish. Conversely, labeling a submission as delivered because the request returned successfully creates false compliance evidence. The audit record should preserve both the observed state and the time at which the application stopped waiting.

How should the system be tested and operated?

The test suite should exercise the event ledger, not just a happy-path controller test. Assert that a duplicate submission is idempotent, that a challenge cannot be used twice, and that a late callback cannot change a completed authentication decision. Replay the same callback twice. Deliver callbacks out of order. Kill the reconciliation worker between reading a status and committing the event. The resulting record should remain explainable.

Operational telemetry needs a separate budget. Keep every security decision and terminal outcome required by the audit policy. Sample repetitive polling logs after the event schema is proven, and aggregate metrics by bounded dimensions. A unique provider reference is valuable for lookup in structured logs, but it is a poor metric key. This is where observability becomes governance: less retained detail can be the more accurate design when the discarded detail is redundant and the retained event is authoritative.

Email has the same evidence boundary. Sender authentication and delivery guidance can affect acceptance, yet an accepted message is not a read receipt. The channel changes; the record model does not.

This design has a real limitation: an append-only ledger, bounded polling, and regional policy metadata require more schema and review work than one mutable status field. A small internal tool with no compliance obligation may reasonably omit that burden. A B2B SaaS login tied to an auditable notice should accept the extra state because the simpler design cannot explain retries, late observations, or the authentication decision.

What should be rejected in review?

Reject a design with one mutable status column as its only history. Overwriting accepted with delivered erases ordering, duplicate attempts, and the difference between an observation and a decision. A current-status read model is useful for a support screen, but it should be derived from an append-only event trail.

Also reject any design that makes a carrier lookup part of the authorization critical path. It couples login latency to a transport system that cannot prove human possession. The appropriate decision rule is narrower: authenticate from the short-lived challenge; use transport observations to reconcile the compliance notice; retain enough context to explain the policy and outcome; and keep secrets out of the evidence stream.

That boundary is the architecture decision. It makes the integration replaceable, the audit record legible, and the telemetry proportionate to what it can actually prove.

References

πŸ“° Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.