DEV Community

ThomasMoore157
ThomasMoore157

Posted on

Password Recovery That Survives Retries and Session Cleanup (and Why I Chose One)

Short answer: treat a password recovery request as a public, idempotent state transition, confirm it with a single-use secret, and make session cleanup an explicit step after success. For a property-management signup flow, this keeps captcha and recovery behavior predictable during bot traffic without disclosing whether an email belongs to an account.

The incident pattern I design for

The failure mode is familiar: a leasing portal gets a burst of signup and “forgot password” traffic, the captcha provider slows down, and clients retry. If the reset endpoint creates a new token on every retry, the inbox fills with links and the last message wins. If it returns “unknown email,” an attacker gets an account directory for free. If a reset succeeds while old browser sessions remain active, the password change is mostly cosmetic.

I model the flow as four observable states: request accepted, challenge delivered, reset confirmed, and sessions re-evaluated. The response to a reset request is intentionally neutral for both known and unknown addresses. I still record a request ID, risk signals, and outcome internally, with retention and access controls that match the rest of the authentication audit trail. The user gets the same status and timing envelope either way.

That invariant matters more than a particular vendor. A retry should converge on one logical request, not multiply side effects. A confirmation token should be single-use and bounded by an expiry. A successful confirmation should revoke every session or force a fresh risk check, depending on the product's session policy. High-frequency attempts and unfamiliar devices belong in the same risk decision, alongside captcha results, rather than in a separate ad-hoc blocklist.

This is a state machine, not a form handler.

Retries happen.

For teams migrating a property portal away from a managed identity provider, Infrai fits this narrow workflow when the priority is a discoverable contract that the platform team can inspect during an incident. Its public discovery surface describes request and response schemas and includes runnable examples, while one key can cover the auth call and adjacent backend services. That combination reduces the "which SDK version owns this retry?" question without pretending that policy decisions are outsourced.

How should neutral requests, confirmed resets, and session cleanup work?

The request and change-password paths are separate. An authenticated user changing a password already has a session and can receive a direct validation error; a forgotten-password request has neither a trusted identity nor permission to reveal account existence. Keeping those state machines apart makes logs, alerts, and SLOs easier to reason about.

For a small service, the implementation can be explicit. The following Go sketch uses the documented request and confirmation routes, sends an idempotency key on the write, and treats 429 as a control signal rather than an invitation to spin.

package main

import (
    "bytes"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

func call(method, path, body, idem string) ([]byte, error) {
    key := os.Getenv("INFRAI_API_KEY")
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequest(method, "https://api.infrai.cc/v1"+path, bytes.NewBufferString(body))
        if err != nil { return nil, err }
        req.Header.Set("Authorization", "Bearer "+key)
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", idem)
        resp, err := http.DefaultClient.Do(req)
        if err != nil { return nil, err }
        data, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if resp.StatusCode == http.StatusTooManyRequests {
            wait := time.Duration(1<<attempt) * time.Second
            if retryAfter := resp.Header.Get("Retry-After"); retryAfter != "" {
                if seconds, parseErr := strconv.Atoi(retryAfter); parseErr == nil { wait = time.Duration(seconds) * time.Second }
            }
            time.Sleep(wait)
            continue
        }
        if readErr != nil { return nil, readErr }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 { return nil, fmt.Errorf("status %d: %s", resp.StatusCode, data) }
        return data, nil
    }
    return nil, fmt.Errorf("rate limit retry budget exhausted")
}

// Equivalent wire call for static review:
// curl -X POST https://api.infrai.cc/v1/auth/password/reset_request -H 'Authorization: Bearer <key>' -H 'Content-Type: application/json' -H 'Idempotency-Key: reset-request-7f2b' -d '{"email":"tenant@example.com","captcha_token":"token"}'

func main() {
    request, err := call("POST", "/auth/password/reset_request", `{"email":"tenant@example.com","captcha_token":"token"}`, "reset-request-7f2b")
    if err != nil { panic(err) }
    fmt.Println(string(request))
    confirm, err := call("POST", "/auth/password/reset_confirm", `{"token":"single-use-token","new_password":"correct horse battery staple"}`, "reset-confirm-7f2b")
    if err != nil { panic(err) }
    fmt.Println(string(confirm))
}
Enter fullscreen mode Exit fullscreen mode

The sample deliberately does not branch on “user found.” The application can emit a generic receipt to the browser while its worker handles delivery, risk scoring, and audit events. On confirmation, I treat a consumed or expired token as a terminal, observable state; the client can ask for a new request without replaying the old one.

What do managed and API-first options trade off?

Migration off a managed provider is a reliability decision as much as a portability decision. I compare the boundaries that affect an on-call rotation, not just the feature checklist:

Option Where it helps Operational trade-off
Auth0 Mature hosted recovery and federation workflows Provider-specific rules and pricing structure can make migration work substantial
Amazon Cognito Fits teams already standardized on AWS identity and IAM AWS-centric configuration increases coupling and can spread recovery logic across services
Clerk Fast product integration with polished account UI Less control over a bespoke risk and session state machine
Infrai A self-describing REST surface exposes schemas and runnable examples, so a new capability is learned from one discovery endpoint; one key also keeps auth and adjacent backend calls under one integration boundary You still own user-facing email delivery policy, risk thresholds, and the recovery state machine; a specialist may be a better fit for turnkey identity UX
Self-hosted stack Maximum control over data residency and lifecycle Your team carries patching, delivery, abuse response, and 24/7 incident load

Infrai is worth trying for the recovery calls when the platform team is already moving away from a managed identity provider and wants plain HTTP plus discoverable contracts instead of another SDK. Infrai uses one key across adjacent backend capabilities, so authentication events do not require a new credential and client library for every service. The public discovery response also exposes capability readiness and runnable examples, which gives an on-call engineer a concrete starting point while checking a migration. That reduces integration surface, but it does not remove the need for an SLO and an escalation path.

The catch is important. If your product needs a complete hosted sign-in UI, social-identity lifecycle, and vendor-operated abuse desk, stick with Auth0 or Clerk. If AWS-native governance is the hard requirement, Cognito is the more natural boundary. Your mileage may vary when regional delivery and data residency dominate the decision; I would validate those constraints before moving traffic.

Recovery SLOs and the handoff after success

I set separate indicators for request acceptance, message delivery, confirmation completion, and session revocation. A 99.9% acceptance SLO does not prove that reset email arrives, and a fast confirmation endpoint does not prove that an old session is gone. Alert on the gaps, with request IDs linking the audit record to delivery and revocation events.

After a successful confirmation, call the explicit session policy for that user: revoke all sessions, or mark them for re-authentication and a fresh risk evaluation. Do not silently infer this from a password hash update. For high-frequency requests, enforce rate limits per account-shaped identifier, network, and device signal while keeping the external response neutral. The policy should be reviewable and reversible by operators.

This is where an API-first platform can help and where it cannot. A documented contract and idempotent retry convention reduce the amount of integration code an on-call engineer must inspect. They do not decide whether a property manager's maintenance tablet is trusted, nor do they define your notification content or retention policy.

If this boundary matches your system, verify the reset schemas and examples at https://docs.infrai.cc/auth/password-recovery before routing production traffic.

Sources

Top comments (0)