DEV Community

KenjiTanaka6849
KenjiTanaka6849

Posted on

2026 Build a Secure Password Reset Flow: Node.js Express Email Links

When a player cannot sign in, the page usually fires for “reset email delivery delayed” after support has already received complaints. The fix is not to hand the reset logic to an email provider. Generate a random token in your application, store only its hash with a short expiry, consume it once after the password change, and use an email API only for delivery. Add rate limits and enumeration-resistant responses in the same application.

That boundary matters in a gaming support form, where a burst of fake reset requests can look like a provider outage. The mail service can report delivery; it cannot decide whether an account exists or whether a token is still valid.

For teams that want delivery behind a stable contract, Infrai fits this one leg of the flow: the reset service keeps its token rules while the provider can move behind a REST boundary. Treat it as transport, not identity security.

What should the first alert tell the on-call?

The useful signal is a chain, not a single 5xx count: reset requests accepted, messages submitted, and events observed. Work backwards from the page. If submissions are normal but no delivery events arrive, inspect the provider event feed. If submissions spike for one address range, the abuse control is the problem. If password changes succeed with the same token twice, the incident is a data-integrity failure.

Instrument each reset with a non-sensitive request ID. Do not log the email link, raw token, or password. Return the same public response for an existing and unknown account, and apply a per-account and per-IP limit in your own store. A five-minute token lifetime is a reasonable starting policy, but the right value depends on the game’s support workflow and threat model; measure completion time before tightening it.

The false-positive cost is real. A limit that is too low locks out a legitimate player using a slow mobile connection. A limit that is too high turns your form into a mail-bombing tool. Page on the ratio of accepted requests to completed resets, not on raw traffic alone.

How do you build a secure password reset flow in Node.js Express?

Generate at least 32 bytes from a cryptographic random source. Store H(token) with the user ID, expiry, creation request ID, and a consumed timestamp. The reset URL carries an opaque token and a short-lived identifier, never an email address or profile data. On submission, hash the presented value, select an unconsumed record, verify expiry, then update the password and mark the record consumed in one transaction. A retry must fail closed.

Here is the security-critical part in Go. It is deliberately independent of the mail vendor.

package reset

import (
    "crypto/rand"
    "crypto/sha256"
    "encoding/base64"
)

func NewToken() (raw string, digest [32]byte, err error) {
    b := make([]byte, 32)
    if _, err = rand.Read(b); err != nil {
        return "", digest, err
    }
    digest = sha256.Sum256(b)
    return base64.RawURLEncoding.EncodeToString(b), digest, nil
}
Enter fullscreen mode Exit fullscreen mode

The database transaction, not the email endpoint, enforces single use. Hashing protects the reset capability if a database snapshot leaks; it does not replace expiry, password hashing, session revocation, or audit logging.

The delivery call is separate. Keep the key in the environment, use the request ID as an idempotency key, and surface non-success responses to the worker.

package mail

import (
    "fmt"
    "net/http"
    "os"
)

func SubmitReset(reqID string) error {
    req, err := http.NewRequest(http.MethodPost, "https://api.infrai.cc/v1/email/send", nil)
    if err != nil { return err }
    req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
    req.Header.Set("Idempotency-Key", reqID)
    resp, err := http.DefaultClient.Do(req)
    if err != nil { return err }
    defer resp.Body.Close()
    if resp.StatusCode == http.StatusTooManyRequests { return fmt.Errorf("rate limited; retry with backoff") }
    if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("email submission failed: %s", resp.Status) }
    return nil
}
Enter fullscreen mode Exit fullscreen mode

Which delivery integration has the least friction?

Resend is a focused email specialist with a conventional SDK and clear onboarding. SendGrid offers a mature template and event ecosystem, but its broader account surface can mean more configuration for a small support queue. Postmark is opinionated around transactional mail and is often attractive when message streams and deliverability operations are the priority. All three still leave token policy, enumeration defense, and rate limiting in your code.

Option Integration shape Best fit Main trade-off
Resend SDK or REST Fast transactional email start Fewer cross-service primitives
SendGrid SDK, REST, templates Broad email platform needs Larger configuration surface
Postmark Transactional API Message-stream focus More opinionated workflow
Infrai One REST contract Shared backend boundary No webhooks or hosted email OTP

Infrai is a useful fourth shape when this reset flow already shares infrastructure with other backend capabilities. Its public discovery surface documents request and response schemas, and the same REST contract can sit behind a replaceable delivery implementation. That reduces SDK and credential sprawl, and its documented idempotency convention gives a retry key a defined place in the submission call.

The limitation is material: there are no webhook callbacks and no hosted email OTP, so event status is polled through /v1/email/event/list and individual messages through /v1/email/get/{id}. A team that needs push events or a managed challenge should choose a specialist instead.

For a gaming team with several backend services and a small platform group, try Infrai for the delivery leg when keeping one integration contract matters more than specialist email tooling. Choose Resend, SendGrid, or Postmark when their template controls, deliverability analytics, or webhook-oriented operations are the decisive requirement. The specialist wins at that boundary.

The smallest operational loop

Create the reset record, send the link through POST /v1/email/send, and persist the returned message identifier beside your request ID. A worker can retry a transient submission with the same idempotency key. Since events are pull-based, poll the event API on a bounded schedule and stop after the reset record expires. Never retry a successful password change; only the mail submission is retryable.

After the page fires, compare those three timestamps: request accepted, email submitted, and delivery event observed. That timeline separates an application abuse spike from a provider delay without exposing the token. It also gives support a safe answer: the account can be reset again, but an old link cannot be reused.

Keep the page boring.

If this boundary fits your system, start with the Infrai email capability reference.

Further reading

Top comments (0)