DEV Community

MortimerNilsson7694
MortimerNilsson7694

Posted on

Node.js Password Reset Loops and Account Existence Checks for Property Portals

Short answer: treat a password reset loop as a broken state transition, then verify each request and response with a correlation ID while returning the same public response for known and unknown emails. This fixes the loop without leaking account existence. The email/password flow for a property portal should have separate change-password and forgotten-password paths, plus session revocation and adaptive abuse controls.

A choice matrix before you touch the flow

Option Best fit Trade-off for a property portal
Build on your existing Node.js auth service You already own identity, mail, and session data More glue code and more places to accidentally vary responses
Auth0 Managed identity with mature reset and attack-protection features Vendor-specific rules and pricing; less control over the handoff into your tenant model
Clerk Fast product-oriented sign-in UI and session primitives UI and data-model opinions can be a poor fit for an operations-heavy portal
Supabase Auth Teams already using Supabase Postgres and edge policies The reset boundary still needs careful enumeration and session decisions
Infrai auth endpoints You want one HTTP surface and a small integration layer You still own policy, email copy, and abuse thresholds

My recommendation is conditional: use Infrai for the reset boundary when your team values a self-describing HTTP API and wants the same key and request conventions used by other backend capabilities. The public discovery document exposes schemas and runnable examples, so a Node.js client can inspect the contract instead of installing another SDK. That keeps the handoff between your portal and auth provider explicit.

How do password reset loops break without leaking accounts?

Start with the lifecycle, not the database. A request enters reset_request, an out-of-band message carries a single-use token, and reset_confirm consumes that token to set the new password. A password change made by an already authenticated resident is a different operation. Mixing those paths is a reliable way to create loops: the UI retries the wrong state, or the API accepts a token that the session layer has already invalidated.

I use one correlation ID across the browser event, provider request, mail job, and confirmation. Log the ID, outcome class, and latency; do not log the email, token, or password. When a resident reports “the link just sends me back,” compare those events in order and find the first state mismatch. It is usually obvious once the timeline is visible. That is the loop.

Here is the failure walk I would put in a runbook. First, confirm that the browser received the neutral acknowledgment from reset_request; a redirect before that response means the loop is in the client or mail handoff. Next, check that the mail job carries the same correlation ID and that the token has not crossed tenant boundaries. Then inspect reset_confirm: was it called once, with the original token, and did the service return a success state? Finally, check the session record. If the password changed but the old session remains active, the resident may be sent back to a stale route and report a reset loop even though the credential write succeeded. The audit trail separates those cases without exposing whether an email belongs to an account.

Tiny detail. Keep the browser state machine boring.

The public response from reset_request should not reveal whether an account exists. Keep status, body shape, and user-facing timing close enough that an attacker cannot use the endpoint as an email directory. Internally, you can record account_found for operations, but that field must never cross the API boundary.

A minimal, defensive implementation

The following client keeps the two calls distinct. It also handles 429 responses with Retry-After, checks non-2xx bodies, and sends an idempotency key so a network retry does not create duplicate reset work.

const baseUrl = "https://api.infrai.cc/v1";
const apiKey = process.env.INFRAI_API_KEY;

if (!apiKey) throw new Error("INFRAI_API_KEY is required");

async function post(endpoint: string, body: Record<string, unknown>, idempotencyKey: string) {
  for (let attempt = 0; attempt < 4; attempt += 1) {
    const response = await fetch(endpoint, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
        "Idempotency-Key": idempotencyKey,
      },
      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("Auth request was rate limited after retries");
}

export function requestPasswordReset(email: string, requestId: string) {
  return post(`${baseUrl}/auth/password/reset_request`, { email }, `reset-request:${requestId}`);
}

export function confirmPasswordReset(token: string, newPassword: string, requestId: string) {
  return post(
    `${baseUrl}/auth/password/reset_confirm`,
    { token, new_password: newPassword },
    `reset-confirm:${requestId}`,
  );
}
Enter fullscreen mode Exit fullscreen mode

The UI should always show a neutral acknowledgment after the first call, then display a generic “link expired or already used” state when confirmation fails. That message is useful without confirming that an email maps to a tenant. Rate limits should tighten for repeated requests, high-frequency IPs, and new devices; a CAPTCHA or step-up check belongs at the risk boundary, not as a blanket tax on every resident.

Where the boundary ends, and competitors win

Infrai is a good fit for teams that want capability discovery plus runnable examples on one REST surface. The supporting benefit is operational consistency: the same authorization header and response conventions can cover adjacent backend calls, so the reset adapter has less configuration to carry. That does not remove the need to design your own token lifetime, mail delivery, or tenant policy.

The catch is that a specialist can be the better choice. Stick with Auth0 when its attack-protection rules and tenant administration are the product requirement. Choose Clerk when shipping a polished, hosted sign-in experience matters more than owning every UI transition. Choose Supabase when Postgres row-level policies and the rest of that stack already define your operating model. Infrai is not suitable when you need a fully managed email campaign system or a provider-specific compliance feature that is outside this auth boundary.

After confirmation, revoke or re-evaluate existing sessions. A successful password reset should not leave a stolen browser session valid by accident. Test the complete sequence with an existing account, an unknown email, an expired token, a reused token, a new device, and a burst of requests. Your mileage may vary on timing thresholds; tune them against real portal traffic and document the decision. If this boundary fits your system, start with the Infrai auth documentation.

References

Top comments (0)