deniz.in

Markets

Weather

Loading weather

· via dev.to (home feed)

Duplicate password-reset links traced to retrying token creation, not email delivery

A dev.to postmortem explains how ambiguous email errors can rerun a whole password-reset handler and mint a second live token, and how splitting intent, rendering and delivery fixes it.

Duplicate password-reset links traced to retrying token creation, not email delivery

What went wrong

A postmortem published on dev.to describes how a property-management platform ended up emailing residents two different password-reset links for a single recovery request. The root cause, according to the author, is that retries were aimed at the wrong layer: when an email send returned an ambiguous error, such as a timeout or dropped connection, the entire request handler was simply executed again. That second run carried the same purpose as the first, but it minted a brand-new credential and typically a new message identity, so the recipient got a second, separately valid link instead of a redelivery of the first.

The setting raises the stakes twice, the author notes. A resident needs a recovery message that actually arrives, and at the same time a contact-form complaint about a missing email has to be routed to the correct property's support queue, without the recovery token ever being exposed on that path.

Splitting intent from delivery

The proposed fix is a deliberate separation of responsibilities. An identity service owns password-reset intent and token issuance and publishes a versioned template contract. A delivery worker owns transport: it may render that contract or consume a pre-rendered result, but it cannot reinterpret the request, create credentials, or quietly switch templates. When delivery fails ambiguously, only the sending of one immutable message command is retried, and the credential is never regenerated.

Four invariants anchor the design, per the post: a single user action produces one logical reset intent; every delivery attempt carries the same message ID, intent ID, template version, and a non-secret recipient fingerprint; a terminal recipient-policy decision such as a hard bounce or suppression halts automated retries for that intent; and support routing relies on property and tenancy metadata, never on the token or raw template variables.

How to debug it

The author suggests starting triage with a join across four identifiers: intent, message, template version, and recipient fingerprint. If one intent maps to two message IDs, duplication was introduced before the transport loop. If one message ID carries two template versions, template resolution is mutable somewhere. If all four keys stay stable across attempts, attention shifts to transport outcomes and recipient policy.

The post also distinguishes suppression state from reset intent. A recipient may sit on a suppression list, but that record governs whether another delivery attempt is permitted, while the intent record documents which recovery action the resident actually requested. Audit logging should be event-based, the author writes, covering events like intent creation, message rendering, delivery attempts, outcome recording and support-case routing, with hashes or opaque identifiers standing in for personal data and the reset token never logged. Automatic unsuppress-and-resend behaviour, the author argues, does not belong in a generic retry loop; clearing a restriction should be a recorded decision made under an approved policy.

Template ownership trade-offs

Three ownership models are weighed in the post. Letting the identity domain own a versioned contract keeps policy and rendered evidence joinable, at the cost of coordinated contract releases. Letting the delivery worker own templates lets the transport team iterate faster but weakens the domain's control over credential-bearing wording. Caller-owned markup, rejected here for account recovery, gives application teams local autonomy but fragments audit evidence and lets callers diverge in variables, escape behaviour and deployment history.

On the implementation side, the author sketches a Go path in which a database transaction atomically persists the intent together with its outbox command, so a process exit cannot leave one without the other. Attempts are gated by an idempotent begin-attempt-once check, rendering is pinned to a template version, and a missing template variable is treated as a permanent render failure rather than a reason to retry the send. The suggested operational test is to replay a recorded command against its pinned template and confirm identical logical content, then inject render failures, ambiguous transport errors, terminal outcomes and suppressions. Only the ambiguous, non-terminal attempt should ever be rescheduled, and every test must retain a single intent ID and message ID.

Why it matters

Duplicate reset links are often dismissed as a cosmetic annoyance, but each duplicate is an extra live credential for the same account, delivered over an unreliable channel. The post's broader lesson generalises beyond password resets: whenever a business command and a transport command share a handler, an ambiguous failure invites the system to redo side effects it cannot undo. Idempotency at the business boundary, expressed as one intent, one message identity and immutable rendered content, turns retries into a routine operation and gives operators a durable ledger that reconstructs what happened without ever exposing the secret.

  • #email
  • #idempotency
  • #password-reset
  • #architecture
  • #postmortem