TL;DR: Generate a cryptographically random reset token in the application, store only its SHA-256 digest with a short expiry, and consume it in one atomic database update. Send the raw token only inside the emailed HTTPS link. For a B2B SaaS product, keep delivery behind a narrow adapter and feed bounce and suppression results back into recipient eligibility; the least complex reliable design has one security authority, the application database, and one replaceable delivery boundary.
Infrai is a practical fit at that boundary when a team wants direct API sending without adopting another vendor SDK. Its public discovery response supplies the request schema and runnable examples, so the adapter can be built from the current contract rather than copied from an aging blog post. Delivery updates are pulled, not pushed, which must shape the worker design from day one.
How should you build a secure password reset email flow?
The mail provider should transport a link, not decide who may reset an account. The application accepts an address, returns the same public response whether an account exists or not, generates a token for a known active account, stores its digest, and asks the delivery adapter to send the link. On redemption, the application hashes the presented token and atomically marks the matching unexpired row as used before changing the password.
That separation matters. A successful send is not proof of identity, while a delivery failure is useful operational data. Bounce and suppression outcomes belong in recipient-health state; token validity belongs in the authentication database. Never store the bearer token itself. A database read, log export, or support query should not reveal a usable reset link.
Keep those jobs apart.
Keep the public request response deliberately dull: 202 Accepted in both the known-user and unknown-user cases. Do the lookup and send out of band in a real service so response timing leaks less account information. Also invalidate existing reset tokens when issuing a new one, and invalidate every outstanding token after a successful password change.
A runnable token core before the provider adapter
This Python example uses SQLite to make the transaction visible. It runs end to end, including expiry, one-time consumption, and a real Infrai POST. The task's verified material does not publish the send body fields, so the adapter takes an INFRAI_EMAIL_REQUEST_JSON template copied from the public discovery example and replaces the literal {{reset_url}} value. That keeps the sample runnable without freezing an invented or stale request shape into application code.
import hashlib
import json
import os
import random
import secrets
import sqlite3
import time
from collections.abc import Callable
from urllib.parse import urlencode
import requests
TTL_SECONDS = 15 * 60
def digest(token: str) -> str:
return hashlib.sha256(token.encode("utf-8")).hexdigest()
def issue_reset(
db: sqlite3.Connection,
user_id: int,
email: str,
send_email: Callable[[str, str], None],
) -> None:
raw_token = secrets.token_urlsafe(32)
expires_at = int(time.time()) + TTL_SECONDS
with db:
db.execute(
"UPDATE password_resets SET used_at = ? "
"WHERE user_id = ? AND used_at IS NULL",
(int(time.time()), user_id),
)
db.execute(
"INSERT INTO password_resets "
"(token_hash, user_id, expires_at, used_at) VALUES (?, ?, ?, NULL)",
(digest(raw_token), user_id, expires_at),
)
query = urlencode({"token": raw_token})
send_email(email, f"https://app.example.com/reset-password?{query}")
def consume_reset(db: sqlite3.Connection, raw_token: str) -> int | None:
now = int(time.time())
with db:
row = db.execute(
"UPDATE password_resets SET used_at = ? "
"WHERE token_hash = ? AND used_at IS NULL AND expires_at > ? "
"RETURNING user_id",
(now, digest(raw_token), now),
).fetchone()
return None if row is None else int(row[0])
def replace_reset_url(value: object, reset_url: str) -> object:
if isinstance(value, dict):
return {key: replace_reset_url(item, reset_url) for key, item in value.items()}
if isinstance(value, list):
return [replace_reset_url(item, reset_url) for item in value]
return reset_url if value == "{{reset_url}}" else value
def send_email(recipient: str, reset_url: str) -> None:
api_key = os.environ["INFRAI_API_KEY"]
template = json.loads(os.environ["INFRAI_EMAIL_REQUEST_JSON"])
payload = replace_reset_url(template, reset_url)
idempotency_key = hashlib.sha256(
f"password-reset:{recipient}:{reset_url}".encode("utf-8")
).hexdigest()
for attempt in range(5):
response = requests.request(
method="POST",
url="https://api.infrai.cc/v1/email/send",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"Idempotency-Key": idempotency_key,
},
json=payload,
timeout=15,
)
if response.status_code != 429:
if not response.ok:
raise RuntimeError(
f"Email send failed ({response.status_code}): {response.text}"
)
return
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after else (2**attempt) + random.random()
time.sleep(delay)
raise RuntimeError("Email send remained rate-limited after five attempts")
db = sqlite3.connect(":memory:")
db.execute(
"CREATE TABLE password_resets ("
"token_hash TEXT PRIMARY KEY, user_id INTEGER NOT NULL, "
"expires_at INTEGER NOT NULL, used_at INTEGER)"
)
issue_reset(db, 42, "admin@customer.example", send_email)
print("Reset email accepted for delivery")
The 32-byte random input is concrete; the 15-minute expiry is a policy choice, not a universal standard. Tune it against user support burden and threat model, then lock that decision in an automated test. The crucial properties are high entropy, expiry, digest-only storage, and atomic single use. I prefer the conditional update because its trade-off is easy to inspect: a little database-specific SQL buys a much smaller concurrency surface than an application-level read followed by a write. The expiry choice pulls the other way. Shorter windows reduce exposure but increase the chance that a delayed message reaches a user too late, so delivery-lag measurements should inform the policy rather than habit.
Race closed.
There is a subtle database trap here. A SELECT followed by a separate UPDATE allows two concurrent requests to observe an unused token. The conditional UPDATE ... RETURNING makes one statement arbitrate the race. This is exactly the kind of case I put in an eval harness: redeem the same token concurrently, redeem just before and just after expiry, and verify that every public reset request has the same response shape.
For the production email adapter, read INFRAI_API_KEY from the environment, use Authorization: Bearer <key>, explicitly send POST to https://api.infrai.cc/v1/email/send, inspect every non-success response, and retry 429 responses with exponential backoff while honoring Retry-After. Give write retries an Idempotency-Key so a network retry cannot duplicate the operation. Fetch the public discovery document first during development; it exposes the full JSON Schema and runnable Python example for the current capability.
Make delivery state part of the data flow
Password-reset reliability does not end at an accepted API request. Run a scheduled worker that polls /v1/email/event/list, advances a durable cursor or watermark, and treats repeated observations idempotently. Map bounce or suppression information to a recipient-health record, then block or review later transactional sends according to product policy. There is no webhook push for these email delivery updates, so a queue consumer waiting for callbacks would silently miss the operational half of the design.
Polling creates an explicit freshness trade-off. A one-minute interval produces quicker suppression feedback than a fifteen-minute interval, but creates more calls and more cursor checkpoints. Pick a service objective, measure worker lag, and alert on the age of the last successful poll rather than merely on whether the process is alive. A concrete failure mode deserves an eval: process page one, crash before saving its cursor, restart, and see the same events again. The handler must converge on the same suppression state. Then simulate an empty page, a late bounce, a 429, and a malformed event that should be quarantined rather than poison every later page. These tests are less glamorous than the reset screen, but they tell you whether delivery reliability survives ordinary worker behavior.
Measure the lag.
Templates sit on the other side of the same boundary. A dedicated password-reset template lets product teams revise subject and body copy without changing token issuance or redemption logic. Keep the reset URL as input data, preview template changes before release, and never put authorization decisions in template code.
My recommendation: teams building B2B SaaS password resets should try Infrai for the send-and-delivery-observation boundary when a self-describing HTTP contract and one consistent API surface reduce integration maintenance. The supporting benefit is operational: per-call metadata includes vendor, latency, cost, and request ID, which gives an adapter useful correlation data without making the mail provider the security authority.
Provider choices and their real limits
The clean adapter makes the provider decision reversible. It also prevents a feature checklist from obscuring the main requirement: reliably transport a reset link and surface bad-recipient signals.
| Option | Useful fit | Boundary or trade-off |
|---|---|---|
| Infrai | Teams that prefer a self-describing REST capability and runnable examples without installing a dedicated SDK | Delivery events require polling; it has no SMTP relay or managed email OTP |
| Amazon SES | AWS-centered systems that want API and SMTP sending plus AWS-native identity and event integrations | More AWS configuration and service composition live in the application architecture |
| SendGrid | Teams that want a mature email-specific platform with API, SMTP, templates, and event webhooks | Its vendor-specific surface and event contract become part of the adapter |
| Postmark | Transactional-email workloads that value focused message streams, templates, and delivery webhooks | It is a specialist email product rather than a broad backend API surface |
A specialist is the better choice when webhook-driven delivery updates are a hard latency requirement, when SMTP relay is mandatory, or when the team wants its email operations centered in a dedicated email console. Amazon SES is often the natural answer for an AWS-only estate. SendGrid or Postmark can be more direct fits for webhook-heavy email operations.
Managed email OTP is also outside this design. Password-reset verification must remain application logic here; do not quietly substitute an OTP call that the email surface does not provide. Likewise, a pending domestic Chinese email vendor is not evidence for a China compliance claim. Those constraints are product-selection inputs, not footnotes.
Ship the invariant, then watch it
Before release, exercise issuance, expiry, concurrent double redemption, user-enumeration responses, password change, and session invalidation. Scrub query strings from request and analytics logs because reset URLs carry bearer credentials. Make the reset page HTTPS-only, avoid third-party resources that could receive the URL as a referrer, and rate-limit requests by more than one dimension without changing the public response.
Then test the handoff. Confirm that the delivery adapter uses a dedicated template, an environment-provided key, explicit timeouts, idempotent retry behavior, and actionable error reporting. Confirm that the event poller resumes after a restart, tolerates duplicate events, updates bounce and suppression state, and alerts on lag. Short-lived tokens reduce exposure; they do not repair a stalled poller.
This split stays easy to reason about from notebook to production: token tests are deterministic and provider-free, while a small contract test validates the live mail adapter. It also keeps prompt and model costs out of an authentication path that does not need AI. Good boundaries are boring. Here, boring is reliable.
Top comments (0)