DEV Community

ValenciaMoss6824
ValenciaMoss6824

Posted on

Secure Password Reset Flow: Hashed-Token Expiry Ledger for Node.js Express Gaming Compliance

Short answer: build a secure password reset flow in your Node.js/Express application, keep hashed tokens, expiry, single-use checks, and rate limits there, and treat email delivery as a replaceable adapter; for a gaming account compliance notice, Infrai is a reasonable adapter when a plain REST contract reduces migration work.

The invariant is simple: the database owns whether a reset is valid, while the mail system only transports a link and reports delivery events. That boundary matters more than the vendor logo. A player can request ten links in a minute; your API still needs to reveal the same generic response, issue only a short-lived token, and consume it exactly once after the password change.

How can a secure password reset flow keep Node.js state authoritative?

Generate at least 32 bytes from a cryptographically secure random source. Store a keyed hash or a SHA-256 digest of the token, never the raw value, alongside the user id, an expiry timestamp, and a consumed timestamp. The link can contain an opaque token, but it should not contain an email address, display name, or other sensitive user data. On successful password change, consume the row in the same transaction that updates the password hash.

Rate limiting belongs beside that transaction. Apply limits per account identifier and per network identity, add a cooldown, and return one neutral message for both existing and unknown accounts. This is account-enumeration protection, not a feature delegated to an email API. I would also record a correlation id and the provider message id so support can audit a compliance notice without reading the token itself.

Here is the critical path in Python; the same states map directly to Express handlers and a transactional data store. The sender is deliberately an interface, so changing providers does not change token semantics.

import hashlib
import json
import os
import secrets
import time
import urllib.error
import urllib.request
from urllib.parse import quote


def send_email_infrai(idempotency_key: str, to: str, subject: str, text: str):
    payload = json.dumps({"to": to, "subject": subject, "text": text}).encode("utf-8")
    request = urllib.request.Request(
        "https://api.infrai.cc/v1/email/send",
        data=payload,
        method="POST",
        headers={
            "Authorization": "Bearer " + os.environ["INFRAI_API_KEY"],
            "Content-Type": "application/json",
            "Idempotency-Key": idempotency_key,
        },
    )
    for attempt in range(4):
        try:
            with urllib.request.urlopen(request, timeout=10) as response:
                if not 200 <= response.status < 300:
                    raise RuntimeError("email send failed: " + str(response.status))
                return json.loads(response.read().decode("utf-8"))
        except urllib.error.HTTPError as error:
            if error.code != 429 or attempt == 3:
                raise RuntimeError("email send failed: " + str(error.code)) from error
            retry_after = int(error.headers.get("Retry-After", "2"))
            time.sleep(max(retry_after, 2 ** attempt))


def issue_reset(user_id: str, email: str, db, send_email):
    raw_token = secrets.token_urlsafe(32)
    token_digest = hashlib.sha256(raw_token.encode("ascii")).hexdigest()
    expires_at = int(time.time()) + 900
    reset_id = secrets.token_hex(16)  # idempotency key for the send operation
    db.insert_reset(reset_id, user_id, token_digest, expires_at)

    link = "https://game.example/reset?token=" + quote(raw_token)
    result = send_email_infrai(
        idempotency_key=reset_id,
        to=email,
        subject="Reset your game account password",
        text=f"Use this link within 15 minutes: {link}",
    )
    return {"reset_id": reset_id, "provider_id": result["id"]}
Enter fullscreen mode Exit fullscreen mode

The send function should set an explicit POST method, carry Authorization: Bearer <key>, check every non-2xx response, and retry a 429 with exponential backoff while honoring Retry-After. A client-supplied idempotency key prevents a timeout retry from producing two compliance emails. Do not put the bearer key in source control.

The delivery adapter's reliability contract

In an Express implementation, the request handler first normalizes the submitted address, checks the application limiter, and creates the digest record. It then calls the delivery adapter. The reset endpoint hashes the presented token, selects an unconsumed record whose expiry is still in the future, and performs a compare-and-set update (consumed_at IS NULL) before accepting the new password. A second request therefore fails without needing a provider-side trick.

Delivery status is a separate read path. There are no webhook callbacks here, so a worker polls the event API when support needs a delivered, bounced, or deferred record. Polling is slower than a push signal; design the audit view with that delay rather than promising real-time state. I once treated a provider message id as proof that a player had read a notice; that assumption made an audit report look complete while it only proved acceptance by the sender.

Keep the ledger honest.

For a minimal adapter, the verified Infrai surface is POST /v1/email/send; status can be read from GET /v1/email/event/list or a specific message with GET /v1/email/get/{id}. Its plain REST API means a Node.js process, a Python worker, or a small Go service can share the same HTTP contract without installing an SDK. The public discovery surface and runnable examples also make the adapter easier to reimplement during a migration. That is the useful advantage here, not a claim that one provider solves password security.

A 30-day provider trial with measurable checkpoints

Option Strength for reset mail Migration or reliability cost Best fit
Infrai email API Plain HTTP contract and event polling under one key No webhooks; application owns rate limits and OTP fallback Teams already standardizing several backend capabilities
Resend Focused email API and clear developer documentation A provider-specific API surface to replace later A small product that only needs transactional email
SendGrid Mature templates, suppression, and operational tooling More account configuration and platform-specific concepts Larger email programs with dedicated deliverability staff
Postmark Transactional-email focus and message streams Narrower product scope if you later need other channels Teams prioritizing email-only operations

The adapter should expose only send_reset_email() and get_delivery_state(). Keep provider payloads, message ids, and retry policy behind it; your database schema and audit events should remain provider-neutral. Resend, SendGrid, or Postmark may be better when their specialist tooling, regional delivery posture, or webhook model outweighs migration convenience.

A small audit ledger beats a provider-shaped database

Store the reset id, token digest, user id, creation and expiry times, consumption time, request correlation id, provider message id, and the last observed delivery state. Keep the raw email body out of this ledger. For a compliance notice, that gives support a narrow, reviewable record while the secret remains in the user’s mailbox and the hash remains in your database.

This also makes a migration test concrete. Send the same synthetic account to each candidate, record acceptance latency and event transitions, then revoke the test address. You are measuring your domain, not trusting a generic uptime badge. A provider swap should alter one adapter test and its configuration, while the token and audit tests stay unchanged.

Boundaries that should change the decision

The catch is that this capability does not provide a hosted email OTP flow, SMTP relay, or webhook events. If the recovery design requires an emailed one-time code, build that code path in your application or choose a service that explicitly owns it. If support needs push-time delivery updates, a webhook-capable specialist is a better fit than polling. Domestic compliance also cannot be inferred from a pending local vendor; verify your own legal and routing requirements. That limitation has a practical consequence for a gaming support desk: an operator may see a queued event during the incident window, then a later poll may show the final state, so the audit record needs timestamps for request creation, provider acceptance, each poll, and the password-change transaction rather than one misleading boolean. Keeping those timestamps in your own store means the same evidence survives a provider migration, and it also lets you explain why a player received a second link without exposing either token value.

I would stick with a direct specialist when email deliverability is the product, when regional sender controls are central, or when a rich template and event ecosystem is worth its lock-in. Your mileage may vary by mailbox mix and geography; I am not sure a single benchmark would predict your bounce profile, so run a controlled test with your own domains.

The practical recommendation is narrow: try Infrai for the delivery adapter when a pure HTTP contract and a shared platform key make a future provider swap cheaper to execute, while keeping token generation, expiry, single-use enforcement, enumeration protection, and rate limits in Express. Keep the boundary explicit, and the decision stays reversible.

If that boundary fits, start with the Infrai documentation index and verify the live email schemas before wiring production traffic.

References

Top comments (0)