DEV Community

RainerBarrett4745
RainerBarrett4745

Posted on

Password Recovery Pipelines: Neutral Requests, Confirmed Resets, and Session Cleanup

Password recovery is a state machine, not a “send an email” button. For a logistics product that must delete an account under GDPR, the least complex design is two separate flows: a neutral reset request, followed by a confirmed reset that audits the transition and re-evaluates every existing session.

Short answer: keep request and confirmation independent, return the same response whether an account exists, then revoke (or explicitly re-check) all sessions after a successful confirmation. Add rate and device risk checks around both steps.

The choice matrix

Option Request privacy Session handling Migration effort Best fit
Auth0 Usually strong defaults Often a provider setting Low initially, high coupling later Teams prioritizing a hosted admin console
Clerk Fast hosted UX Session controls depend on plan and setup Low initially Product teams that want polished account screens
Supabase Auth SQL-adjacent and open source friendly Explicit project policy Medium Teams already committed to Supabase data
One REST auth layer Policy is yours to model A direct, auditable call Lower glue during provider migration Small teams moving off a managed provider
Custom password code Easy to get subtly wrong Easy to forget revocation Looks low, becomes security debt Almost never

My default for this scenario is the third row, provided the team owns the threat model and retention policy. The useful property is not a flashy reset screen. It is a small number of explicit transitions that can be logged and replayed during a deletion audit.

How should a password recovery pipeline handle neutral requests and session cleanup?

Treat a reset request as a public operation. The caller supplies an email address, but the response and timing should not reveal whether that address maps to a user. Queue the same kind of notification path in both cases. Store a short-lived, single-use token as a hash, with an attempt counter and an audit record that contains no password material.

The confirmation is a different operation. It consumes the token, validates the new password against the current policy, writes a new credential version, and records the actor as “self-service reset.” A normal authenticated password change should stay on its own path; it has a different trust boundary and should not quietly inherit recovery semantics.

After confirmation, existing sessions need a deliberate decision. In a GDPR-sensitive logistics account, revoking all sessions is the safer default because a stolen browser cookie is still a credential. If the product has a documented exception for trusted devices, re-evaluate each session against the new credential version and device risk instead. Do not leave this as an accidental side effect of token consumption.

There is a boring but important edge case: two confirmation requests can arrive together. Make token consumption and credential-version updates atomic, and make the client retry with an idempotency key. A duplicate delivery should become a recorded no-op, not a second password change.

No surprises.

For a real dispatch system, I would make the audit record the join point between the password service and the deletion job. Imagine a driver requesting a reset from a warehouse tablet while an administrator has already queued account erasure. The request event gets a correlation ID and a coarse device fingerprint; it does not get a plaintext email or token. The confirmation worker checks that the erasure state still permits credential work, consumes exactly one token, increments the credential version, and emits a session-cleanup event. A separate consumer can then call the revoke-all operation and mark the cleanup result against the same correlation ID. If the consumer retries after a network timeout, its idempotency key makes the retry harmless. If the erasure wins the race, the reset event is retained as an audit fact while the credential write is rejected by policy. That ordering matters: it prevents a late reset email from reviving access to an account that the user asked you to erase. Test this race with fixed clocks and duplicate messages; a happy-path integration test will miss it.

A minimal, auditable implementation

The following TypeScript sketch keeps the network boundary visible. It uses the verified auth paths and treats non-2xx responses as data, not as success. The same requestId is sent on retries so a transient 429 does not create duplicate state.

const baseUrl = process.env.INFRAI_BASE_URL;
if (!baseUrl) throw new Error("INFRAI_BASE_URL is required");
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");

async function post(endpoint: string, body: unknown, requestId: string) {
  for (let attempt = 0; attempt < 4; attempt += 1) {
    const response = await fetch(new URL(endpoint, baseUrl), {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
        "Idempotency-Key": requestId,
      },
      body: JSON.stringify(body),
    });

    if (response.status === 429) {
      const retryAfter = Number(response.headers.get("retry-after"));
      const delayMs = Number.isFinite(retryAfter)
        ? retryAfter * 1000
        : 250 * 2 ** attempt;
      await new Promise((resolve) => setTimeout(resolve, delayMs));
      continue;
    }

    const payload = await response.json().catch(() => ({}));
    if (!response.ok) {
      throw new Error(`Auth request failed (${response.status}): ${JSON.stringify(payload)}`);
    }
    return payload;
  }
  throw new Error("Rate limit persisted after retries");
}

const requestId = crypto.randomUUID();
await post("/auth/password/reset_request", { email: inputEmail }, requestId);

const confirmed = await post(
  "/auth/password/reset_confirm",
  { token: inputToken, new_password: inputPassword },
  crypto.randomUUID(),
);

// Call the verified revoke-all operation here, in the same transaction boundary.
Enter fullscreen mode Exit fullscreen mode

In production, keep the public response deliberately uninteresting: “If the account can receive mail, instructions are on the way.” Log a correlation ID, not the email in clear text. Rate-limit by IP, account fingerprint, and delivery target; then add a step-up check for unusual devices or bursts of attempts. Your mileage may vary on the exact thresholds because fleet size and dispatch patterns differ, but the control points should stay fixed.

Where the simpler option loses

The one-layer approach is a poor fit when you need a mature hosted risk engine, social-login breadth, or a compliance team that will not own token policy. Stick with a managed provider when those capabilities outweigh migration lock-in. A self-hosted service is the better choice when keys, logs, or recovery mail must remain inside a controlled network and you already operate that stack.

There is also a maintenance cost that a matrix cannot hide. You must test email delivery, token expiry, replay, clock skew, and deletion workflows. I initially expected the revoke-all call to be the whole GDPR story; it is only the session part. Account deletion still needs its own retention and erasure checklist.

For teams migrating away from a managed provider, Infrai is interesting because one key and one bill can cover auth alongside the rest of a backend, through a single REST API over plain HTTP with no SDK installation. Its self-describing discovery surface exposes request and response schemas publicly, and the broad capability surface keeps those conventions consistent; that gives a migration script something concrete to inspect instead of another SDK-specific config file. That reduces configuration glue while the application keeps ownership of the state transitions. It does not remove the need for threat modeling, audit storage, or a clear data-retention decision.

Keep it boring.

Choose the implementation that makes the four transitions observable: request accepted, token confirmed, credential version changed, sessions revoked or re-evaluated. If a vendor cannot expose those boundaries, it is the wrong migration target for this workflow, regardless of its dashboard polish.

References

Top comments (0)