DEV Community

UlyssesBlack2385
UlyssesBlack2385

Posted on

Password Reset Email and Signup Verification — 7 DKIM/SPF Token-Link Checks

Short answer: treat the signup verification email as an auditable security transaction, not a message-send feature. Generate a single-use token on the server, put only a short-lived opaque link in the template, authenticate the sending domain with SPF, DKIM, and DMARC, and page on the evidence that proves a user could not receive or redeem the link. The same boundary works for password reset email, but healthtech signup adds a compliance question: can you show who requested the link, which policy version rendered it, and when redemption happened?

The page usually fires late. A queue has filled, a delivery provider has accepted less mail than expected, or the verification endpoint sees a sudden rise in expired links. At 03:00, a dashboard full of green send calls is not evidence that a patient received a usable link. I have been woken by alerts that meant nothing and missed the one that mattered; the useful question is always: what page fired, and what user action did it represent?

What should a transactional email API prove before a signup link is trusted?

Start with an alert-to-action trace. A signup request creates an account-intent record, the application generates a token, a mail worker submits a message, the recipient's domain evaluates SPF/DKIM/DMARC, and the user redeems the link. Every transition needs a correlation ID. A successful API response proves only that one hop accepted work. It doesn't prove inbox placement, rendering, or redemption.

For a healthtech system, retain the minimum evidence needed by the policy: request timestamp, account-intent identifier, template revision, sending-domain identity, token hash, delivery provider response, and redemption timestamp. Do not store the raw token. The log should be useful during an audit while remaining useless to somebody who gains read access to logs.

The threshold that wakes the on-call should be tied to the action, not to a vanity metric. A spike in accepted messages is harmless if redemption is steady. A small change in redemption rate may be serious if it affects one clinic, one domain, or one template revision. Slice alerts by recipient domain, template revision, and region before paging a human.

That is the instrumentation change: emit one event at each boundary, then derive a trace such as intent_created -> message_accepted -> message_delivered -> link_redeemed. The missing event is the signal. A generic “email failed” counter hides whether the failure happened before token creation or after the user clicked.

How do DKIM, SPF, custom domains, templates, and token links fit together?

SPF authorizes sending infrastructure; DKIM signs the message; DMARC evaluates alignment between the visible From domain and those authenticated identities. Configure them as one policy, and verify alignment with a real message header rather than trusting a provider dashboard. A custom domain is an ownership boundary, not a deliverability guarantee.

Templates belong in version control. Render a text alternative and an HTML part from the same template revision, include the product name and a support path, and keep the link host on a domain your security team owns. Do not put diagnosis, access tokens, or other sensitive health data in the subject line or URL query string.

The token itself should be random, single-use, scoped to one account-intent, and rejected after a deliberately short lifetime. Store a keyed hash, compare it in constant time, mark it consumed in the same transaction as verification, and make a second click return a neutral result. That last behavior matters: an attacker should not learn whether a token was valid, used, or attached to a real account.

Here is the security boundary in Go. The transport that sends the message can be swapped later; the token and evidence rules stay in the application.

package verify

import (
    "crypto/hmac"
    "crypto/rand"
    "crypto/sha256"
    "encoding/base64"
    "errors"
    "time"
)

type Record struct {
    AccountIntent string
    TokenHash     [32]byte
    ExpiresAt     time.Time
    ConsumedAt    *time.Time
}

func NewToken(accountIntent string, now time.Time, ttl time.Duration, secret []byte) (string, Record, error) {
    raw := make([]byte, 32)
    if _, err := rand.Read(raw); err != nil {
        return "", Record{}, err
    }
    mac := hmac.New(sha256.New, secret)
    mac.Write(raw)
    mac.Write([]byte(accountIntent))
    var digest [32]byte
    copy(digest[:], mac.Sum(nil))
    return base64.RawURLEncoding.EncodeToString(raw), Record{
        AccountIntent: accountIntent,
        TokenHash:     digest,
        ExpiresAt:     now.Add(ttl),
    }, nil
}

func Matches(stored [32]byte, accountIntent, token string, secret []byte) bool {
    raw, err := base64.RawURLEncoding.DecodeString(token)
    if err != nil {
        return false
    }
    mac := hmac.New(sha256.New, secret)
    mac.Write(raw)
    mac.Write([]byte(accountIntent))
    return hmac.Equal(stored[:], mac.Sum(nil))
}

func Redeem(record Record, accountIntent, token string, now time.Time, secret []byte) error {
    if record.AccountIntent != accountIntent || record.ConsumedAt != nil || !now.Before(record.ExpiresAt) {
        return errors.New("neutral verification result")
    }
    if !Matches(record.TokenHash, accountIntent, token, secret) {
        return errors.New("neutral verification result")
    }
    return nil // Persist consumption atomically with the account verification.
}
Enter fullscreen mode Exit fullscreen mode

The code intentionally does not send mail. That separation lets a worker retry a transient submission without minting a second credential, and it gives compliance reviewers a clear record of what happened before an external API was called.

Which failure should page first: delivery, redemption, or the link service?

Work backward from the action. If the message was accepted but no redemption event arrives, check the recipient domain, link host, and template revision before raising the send timeout. If redemption succeeds but the account remains unverified, page the database transaction or session boundary. If the link endpoint is slow, the email system may be innocent.

The noisy failure mode is retry multiplication. A worker retries a timeout, generates a new token on each attempt, and sends several valid-looking messages. The user clicks the oldest one; support sees an “expired” report; the dashboard records successful submissions; and an auditor later has to reconstruct which template and DNS identity were live for each attempt. If the queue retries after the provider has accepted the message but before the worker receives its response, the duplicate is especially hard to distinguish from a real resend. Generate once, persist the hash, and make retries idempotent by account-intent and template revision. The retry record should point back to the same evidence trail instead of creating a second security event.

No shortcut.

The other trap is over-broad suppression. A bounce from one recipient domain should not silence every verification message, while an authentication failure should stop the campaign until the DNS and alignment evidence is corrected. Your mileage may vary with local compliance rules, so have the policy owner define retention and suppression windows rather than burying them in worker code.

What is the trade-off between a hosted API and an owned mail path?

A hosted transactional email API can provide queueing, feedback events, and domain signing without your team operating SMTP. The trade-off is an additional processor boundary: you must document what metadata leaves the healthtech environment, how long it is retained, and how a provider outage changes the user journey. An owned mail path gives control over that boundary but transfers reputation management, bounce handling, and DNS operations to your team.

Choose the hosted path when the evidence contract, regional processing, and export format are acceptable and you can fail closed on missing feedback. Keep an owned path when policy requires custody you cannot delegate, or when the provider cannot expose the events your audit needs. Neither choice fixes a token that is long-lived, reusable, or logged in plaintext.

The false-positive cost is real: page too early and the on-call starts muting alerts; page too late and users repeat signup, generating duplicate intents and more sensitive records. Test the complete path with a controlled domain, inspect raw headers for SPF/DKIM/DMARC alignment, and exercise the expired and already-consumed cases in every template revision. The goal is a page that names the user action at risk, not a green dashboard with no explanation.

References

Top comments (0)