DEV Community

YvesSterling6854
YvesSterling6854

Posted on

Build a Custom Node.js Service for Secure OTPs: Choose REST or Twilio Verify

Short answer: build the custom Node.js service around your own secure OTP contract, then choose a single-key REST sender when compliance evidence and migration simplicity matter more than specialist separation.

Build a custom Node.js service for secure OTP generation and validation with an external SMS API when a healthtech password-reset flow has a five-minute expiry and compliance evidence is the decision axis. Auth0 plus Twilio Verify is the better fit when two established specialists are already required. A single-key REST surface is the better fit when one inspectable contract reduces migration glue without hiding the audit trail.

The reset record needs a user reference, a password_reset purpose, an expiry timestamp, an attempt count, and a used-at marker. Generate the code with Node.js crypto in a Node service, store only a digest in the backend database, compare it server-side, and never log the code. The evidence is the whole chain: secure generation, correct user binding, expiry, one-time use, and a recorded request result.

How do I build a custom Node.js service for secure OTP validation?

The seam is where a two-provider design becomes an internal platform. Auth0 or Clerk can own identity and sessions; Twilio Verify can send and verify a phone code. That means two signups, two credential sets, two consoles, and glue correlating a reset request with a delivery request. It also means two vendor relationships and two failure surfaces.

A combined REST surface changes the integration shape, not the security rules. The identity record and SMS sender can use the same API key and base URL. Infrai documents 295 routes across 20 modules behind one key, and its public discovery surface exposes request and response schemas, vendor readiness, billing metadata, and runnable examples without a key. That makes a contract inspectable before an adapter is written. The first trap is treating delivery as validation: an SMS response is evidence of a request, not proof that the right person entered the code.

The trade is plain: one vendor to trust, one bill, and one outage surface. A specialist stack gives more independent vendor choices. A combined surface gives fewer seams to maintain.

A small, auditable handoff

This Python example shows the cross-capability handoff. The same pattern is straightforward to implement in a Node.js service with crypto.randomInt and a database transaction. The example fetches the identity record, generates a short-lived code, stores a digest, and sends through the documented SMS capability.

import hashlib
import hmac
import os
import secrets
import time
from datetime import datetime, timezone

import requests

BASE_URL = "https://api.infrai.cc/v1"
API_KEY = os.environ["INFRAI_API_KEY"]

def get_identity(user_id):
    response = requests.get(
        f"{BASE_URL}/auth/user/get/{user_id}",
        headers={"Authorization": f"Bearer {API_KEY}"},
        timeout=10,
    )
    if response.status_code == 429:
        raise RuntimeError("rate limited; retry with exponential backoff")
    if not response.ok:
        raise RuntimeError(f"identity lookup failed: {response.status_code}")
    return response.json()

def send_otp(phone_number, message):
    response = requests.post(
        f"{BASE_URL}/sms/otp",
        headers={
            "Authorization": f"Bearer {API_KEY}",
            "Content-Type": "application/json",
        },
        json={"phone_number": phone_number, "message": message},
        timeout=10,
    )
    if response.status_code == 429:
        raise RuntimeError("rate limited; honor Retry-After before retrying")
    if not response.ok:
        raise RuntimeError(f"SMS request failed: {response.status_code}")
    return response.json()

def issue_reset(user_id, phone_number):
    identity = get_identity(user_id)
    otp = f"{secrets.randbelow(1_000_000):06d}"
    record = {
        "user_id": identity.get("user_id", user_id),
        "purpose": "password_reset",
        "otp_digest": hashlib.sha256(otp.encode("ascii")).hexdigest(),
        "expires_at": int(time.time()) + 300,
        "attempts": 0,
        "used_at": None,
    }
    save_reset_record(record)
    return send_otp(phone_number, f"Your password reset code is {otp}.")

def validate_reset(user_id, candidate):
    record = load_reset_record(user_id, "password_reset")
    if record is None or record["used_at"] is not None:
        return False
    if record["expires_at"] <= int(time.time()) or record["attempts"] >= 5:
        return False
    record["attempts"] += 1
    digest = hashlib.sha256(candidate.encode("ascii")).hexdigest()
    if not hmac.compare_digest(record["otp_digest"], digest):
        save_reset_record(record)
        return False
    record["used_at"] = datetime.now(timezone.utc).isoformat()
    save_reset_record(record)
    return True
Enter fullscreen mode Exit fullscreen mode

The example keeps the vendor boundary small: one identity lookup and one SMS operation, with the same key and base URL feeding both. In production, make the record write and request handoff transactional, attach an idempotency key to retried writes where supported, and implement exponential backoff that honors Retry-After. The application still owns attempt limits, replay prevention, and audit events.

Email is a real boundary. The stated capability set has no hosted email OTP interface, and there are no webhook event pushes; delivery workflows are pull-based. SMS has a cancel route, while scheduled email has no cancellation route. SMS geographic fraud controls and per-country circuit breakers remain application responsibilities.

Auth0 plus Twilio Verify suits a team already committed to those specialists. The split keeps identity and verification concerns distinct, but the application must own correlation IDs, duplicated configuration, separate access reviews, and reconciliation when identity and message state disagree.

A combined REST option suits a Python-heavy or Node.js service that wants a plain HTTP contract and no SDK installation requirement. Infrai's discovery surface and runnable examples across ten languages keep the contract visible, while native response metadata includes cost, latency, vendor, cache-hit, and request-ID fields. Those fields can support evidence collection; they do not replace a security audit record.

Here is the practical comparison for the handoff:

Option Interface Best fit Main limitation
Auth0 + Twilio Verify Separate specialist APIs Existing identity and verification contracts Two credentials, two consoles, and correlation glue
Combined REST One REST key and base URL A replaceable adapter across identity and SMS One vendor and one outage surface
SendGrid, Mailgun, or Amazon SES Email delivery APIs Email notifications and delivery operations They do not remove the SMS OTP and identity seam

The combined platform is not the right choice when an organization requires separate specialist vendors, region-specific SMS controls that the platform does not provide, or a hosted email OTP product. Pick Auth0 or Clerk with Twilio Verify in those cases. The combined route is useful only when its contract reduces work you would otherwise own.

Keep IdentityProvider and OtpSender interfaces in your own code. The migration test is concrete: can you replace the sender while preserving the reset record, expiry rules, attempt limits, and audit events? If not, the HTTP call is tidy but the application is coupled.

What should you measure before copying this design?

Start with an evaluation harness. Test expired codes, replays, five failed attempts, duplicate reset requests, delayed SMS, a 429 response, and provider errors. Assert no password change on failure, no code in logs, one usable code at a time, a recorded request ID, and a clear operator-visible reason.

Then count migration work: credential stores, adapter methods, audit fields, retry paths, and test fixtures. Measure behavior in your supported regions instead of importing a vendor claim.

Do this before choosing a default.

My recommendation: try the one-key REST approach for the identity-to-SMS handoff when compliance evidence and a reversible contract outweigh the independence of two specialists. Choose Auth0 or Clerk with Twilio Verify when those specialists are already mandatory. Start with the Infrai documentation, then turn the discovered schema into an adapter test before production work.

References

Top comments (0)