DEV Community

xanderblack5716
xanderblack5716

Posted on

5 Compliance Records for SMS OTP API Login Code Limits (Before Seller Access)

Short answer: For a Node.js SMS OTP login API, keep the resend cooldown, code verification state, and rate limit in your application, then record five compact events that prove what the system decided without retaining the OTP itself.

In an edtech marketplace, the constraint is sharper than “send a text.” A seller receives a new-order notice, signs in, and opens student or purchaser details. The team may later need to show that the phone challenge was issued, throttled, accepted, or rejected under a consistent policy. Delivery receipts alone can't establish that chain. Full request logs can, but they also accumulate phone numbers, high-cardinality labels, and more compliance exposure than the login needs.

The design goal is therefore a small, explicit authentication ledger. It should answer who initiated a challenge, which policy version made each decision, and whether login completed. It shouldn't become a replay kit.

How should a Node.js SMS OTP login API verify codes under rate limits?

Model the flow as two operations: request a challenge and verify a submitted code. The request operation checks a per-account cooldown plus broader abuse limits before any SMS API call. The verify operation loads the active challenge, compares the submitted value through the chosen verifier, increments the attempt counter on failure, and consumes the challenge on success. NIST SP 800-63B requires a verifier to accept a given OTP only once while it is valid, and it requires rate limiting when the authenticator output has less than 64 bits of entropy.

Keep those controls server-side. A disabled button and a browser timer improve the interface, but another client can ignore both. A practical policy might allow a resend only after 60 seconds, expire a challenge after 10 minutes, and stop verification after 5 failed attempts. Those are example product settings, not universal security constants; the policy version belongs beside every decision so an auditor can distinguish yesterday's rule from today's.

Use one server clock and one atomic state update. Otherwise two concurrent requests can both observe an expired cooldown, both send, and both record themselves as permitted. The record should identify a stable internal account, not use the raw phone number as a metric label. This matters quickly: 200,000 sellers multiplied by result, country, route, template, and policy labels creates a cardinality problem long before the log volume looks large.

Don't retry blindly.

The application can return retry_after_seconds after a cooldown rejection and a generic verification failure after a wrong or expired code. Keep the detailed reason in restricted audit storage, since exposing “account exists,” “code expired,” and “attempts exhausted” as distinct public responses gives an attacker a better oracle. NIST also treats the public switched telephone network as a restricted authenticator and tells verifiers to consider signals such as SIM changes and number porting. That is a reason to preserve a path to a stronger factor for higher-risk access, not a reason to collect every telecom signal indefinitely.

Treat compliance evidence as a five-event state machine

The smallest useful ledger has five event types: challenge_requested, challenge_blocked, message_handed_off, verification_failed, and verification_succeeded. Each event carries a random challenge ID, an internal seller ID or keyed pseudonym, the policy version, a coarse region if policy requires it, the event time, and a reason code. The handoff record may include the messaging provider's opaque message ID. None of these records needs the OTP value.

That boundary is deliberate. Hashing a six-digit code doesn't make it safe archival evidence because the input space is small. Store only the verifier material needed during the short validity window, isolate it from analytics, and delete it when the challenge is consumed or expires. The long-lived ledger records the decision, not the secret.

An order ID also doesn't belong in every authentication event. Link the successful login session to the order-notification workflow in a separate authorization record. This keeps an investigation possible while avoiding a single telemetry row that joins seller identity, phone delivery, login result, and purchaser activity. The trade-off is an extra lookup during an audit. That cost is preferable to copying commercial and authentication context into every log sink.

There is one more distinction: an SMS handoff is not proof that a person received the message. A delivery status can support operational diagnosis, while the consumed challenge proves that the verifier accepted the code. Name the events accordingly. Precise names prevent a dashboard from turning “provider accepted request” into “user authenticated.”

Exercise the API contract before wiring it into the login route

The transport contract can stay independent of the SMS provider. The Node.js service owns challenge state and exposes two internal operations; the messaging adapter receives only the destination and rendered text after policy checks pass. Before connecting the web route, exercise that contract with curl so concurrency, idempotency, and response fields are visible without a UI.

curl --request POST 'https://auth.example.edu/otp/request' \
  --header 'Content-Type: application/json' \
  --header 'Idempotency-Key: order-login-7f3a-request-1' \
  --data '{
    "seller_id": "seller_7f3a",
    "purpose": "view_new_order"
  }'

curl --request POST 'https://auth.example.edu/otp/verify' \
  --header 'Content-Type: application/json' \
  --data '{
    "challenge_id": "challenge_01JEXAMPLE",
    "code": "123456"
  }'
Enter fullscreen mode Exit fullscreen mode

The first response should contain a random challenge ID, an expiry time, and the next permitted resend time; it should never echo the generated code. Repeating the request with the same idempotency key must refer to the same logical attempt instead of sending another message. The second operation should atomically move an active challenge to either another failed attempt or a consumed state. A second successful verification against the same challenge must be rejected, matching the single-use property in NIST's guidance.

The literal 123456 above is fixture data for a nonproduction contract test. Production tests should inject a fake message adapter, capture the generated code inside the isolated test process, and assert state transitions. That yields deterministic coverage without sending a text or placing secrets in a shared CI log. Tests also need two simultaneous resend requests, two simultaneous correct verifications, expiration at the time boundary, and exhaustion of the attempt limit. It's easy to test the happy path and leave the races untouched.

At the HTTP edge, map policy outcomes consistently: successful creation, cooldown rejection with retry_after_seconds, generic invalid verification, and temporary dependency refusal. The exact status-code convention is a local API decision, so document it in the contract and don't let individual route handlers invent variants. Your mileage may vary on whether a blocked request receives a challenge ID; omitting it reduces correlation material exposed to an untrusted client, while returning an opaque stable ID can simplify support diagnostics.

Count bytes, cardinality, and retention before collecting telemetry

Start with a retention equation, not a dashboard. If the five-event ledger averages 350 bytes per event after serialization and indexing overhead is excluded, 1 million login attempts that generate an average of 2.4 events produce about 840 MB of raw event payload. Replication, indexes, and secondary exports multiply that figure. The exact storage result depends on the database and compression — measure it with a representative batch — but the equation makes every extra field visible:

attempts x events per attempt x bytes per event x retention windows x copies

Cardinality is a separate bill. Event type, outcome, policy version, and coarse route are reasonable metric dimensions because each has a bounded set. Seller ID, challenge ID, provider message ID, phone hash, and order ID belong in searchable logs or an audit store, not time-series labels. Sampling doesn't fix a label set that grows with every seller. It only makes the resulting investigation incomplete.

Keep security decisions at full fidelity for the audit window that counsel and the system owner approve. Sample verbose transport diagnostics more aggressively, because their purpose is aggregate delivery analysis rather than proof of a particular login. I'm not sure any generic retention number can be defensible across jurisdictions and contract terms; the answer comes from the applicable obligation, deletion requirement, incident-response window, and measured investigation needs. Record that decision. A policy with no owner and no deletion job is merely an aspiration.

For example, the long-lived event can retain verification_failed with reason=invalid_or_expired, while a short-lived restricted diagnostic stream distinguishes expiry from mismatch. That split keeps the compliance chain intact and limits sensitive detail. It also prevents a routine analytics query from becoming an account-enumeration dataset.

Email fallback needs its own evidence model. DMARC, defined in RFC 7489, lets a domain publish handling policy and receive authentication feedback based on SPF and DKIM alignment. It can substantiate domain-authentication policy; it cannot prove that a recipient read an order notice or that an OTP was verified. If email carries only a marketplace notification, record its handoff separately. If email becomes an authentication factor, assess and document that factor as a distinct control rather than treating DMARC reports as login evidence.

Less wins here.

Roll out with shadow decisions and an explicit escape hatch

Deploy the state machine first with a fake adapter and shadow rate-limit decisions, then compare those decisions with existing traffic without blocking sellers. Next, enable cooldown enforcement for a small cohort, verify that audit events reconcile with authentication outcomes, and expand only after deletion jobs and access controls have been exercised. A compact rollout check is enough: race tests pass, event counts reconcile, restricted fields stay out of metrics, retention deletion is observable, and the order-notification path still works when authentication is denied.

SMS OTP isn't a good fit when the risk assessment requires phishing-resistant authentication, when sellers cannot reliably receive SMS, or when recovery would depend on the same phone channel. Keep a stronger factor or recovery path for those cases. The catch is that running two factors adds support and evidence complexity; it doesn't justify weakening the SMS controls or retaining every diagnostic forever.

The migration boundary should remain the messaging adapter. Challenge state, resend cooldowns, rate limits, verification, and audit events stay in the Node.js application, so replacing the delivery provider does not rewrite the security policy. That division also makes the compliance claim narrow and testable: the application proves its decisions, while transport evidence describes message handling and nothing more.

References

Top comments (0)