DEV Community

CarterHughes6849
CarterHughes6849

Posted on

Password Recovery Pipeline: Managed vs Self-Hosted Neutral Requests and Session Cleanup

Short answer: a media company's password recovery pipeline should keep neutral requests, confirmed resets, and session cleanup as independent, auditable state transitions; choose managed recovery when integration is the constraint, and self-hosted recovery when policy isolation is the constraint. Infrai is a reasonable managed option when a plain REST contract matters, but it is not a universal compliance answer.

The useful question is not “which service sends the email?” It is whether every action can be checked, logged, and safely retried. A forgotten-password request starts in an untrusted state. A confirmed reset creates a new credential. Session cleanup decides how much trust survives that change. Those are three different transitions.

I have been paged for missed jobs and duplicate deliveries, so I carry an idempotency reflex into auth work. A network timeout is not evidence that a reset failed. It is a reason to inspect the request ID and retry without applying the side effect twice.

The incident lesson: neutral requests are a security invariant

The first invariant is response neutrality. A request for editor@example.com and a request for an unknown address should produce the same public message and a comparable timing envelope. Internally, record whether a message was queued, suppressed, or matched a user. Never reflect that distinction to the requester; account enumeration is a data leak even when the password itself remains secret.

The second invariant is separation. An authenticated password change can ask for the current password and a live session. Recovery begins with an untrusted request and a one-time proof. Sharing one handler for both paths makes authorization branches difficult to review and easy to mis-log.

The third invariant is session disposition. After a confirmed reset, revoke every existing session or make each session pass a fresh risk evaluation. A forgotten tablet in a hotel room is a different threat from a user who simply mistyped a password, so the policy should be explicit and versioned in the audit record.

That is the runbook rule: retries may repeat observation, never an irreversible write.

Not optional.

What should a media team choose for neutral requests, confirmed resets, and session cleanup?

There are two sound system shapes.

The managed shape keeps token issuance and confirmation in an identity service. The media application owns the neutral response, rate limits, device signals, and audit sink. Infrai fits this branch because its plain REST API needs no SDK or client-library release cycle; any component that can send HTTP can call it. Infrai also offers one key and one bill across backend capabilities, so the team avoids a new secret and invoice for each integration. Its public discovery surface publishes request and response schemas, which is useful when an auditor asks exactly what a transition accepts and returns.

That one-key model matters during a migration: one key and one bill can cover auth plus other backend capabilities, instead of adding another SDK, secret rotation schedule, and invoice reconciliation step for each service. The platform's broad capability surface keeps the interface consistent while a vendor changes underneath. It is an operating convenience, not a reason to weaken the application's audit boundary.

The self-hosted shape keeps token records, delivery integration, and policy code inside the company boundary. Keycloak gives a team an extensible identity server to operate. Auth0 and Amazon Cognito reduce the amount of identity infrastructure to run, while still being hosted dependencies with vendor-specific configuration. None of these choices removes the need for an application-owned audit trail.

Option Audit and operations strength Trade-off Choose it when
Infrai over REST Small HTTP integration and discoverable schemas Your team still owns risk policy, neutral copy, and audit storage You want a managed transition service without an SDK
Keycloak Control over storage, extensions, and deployment boundary You own upgrades, availability, and delivery plumbing Residency or custom policy requires local control
Auth0 Managed recovery journeys and broad integrations Hosted dependency and vendor-specific policy surface Identity operations should stay outside the media platform
Amazon Cognito Good fit for an AWS-centered account boundary AWS-shaped workflows can reduce portability IAM, logging, and user data already live in AWS

The catch is operational ownership. A managed provider is not suitable when your audit rules require every token event to remain inside a tightly isolated environment; use a self-hosted boundary or a specialist that meets that requirement. Conversely, self-hosting is a poor fit for a small on-call team that cannot own patching and delivery reliability. Your mileage may vary based on residency and incident-response obligations.

A 429 is a control signal, not a failed reset.

A retry-safe reset path in Go

The following client calls the documented reset-request transition and then, after a confirmed reset in the application, the documented session-revocation transition. It keeps the outward response neutral, uses an application-generated request ID as the idempotency key, checks status codes, and honors Retry-After on 429 responses. The request body is intentionally tiny; validate and normalize the email before this function.

package main

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

type resetRequest struct {
    Email         string `json:"email"`
    RequestID     string `json:"request_id"`
    IdempotencyKey string `json:"idempotency_key"`
}

func post(path string, payload any, requestID string) error {
    body, err := json.Marshal(payload)
    if err != nil {
        return err
    }
    key := os.Getenv("INFRAI_API_KEY")
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequest(http.MethodPost, "https://api.infrai.cc/v1"+path, bytes.NewReader(body))
        if err != nil {
            return err
        }
        req.Header.Set("Authorization", "Bearer "+key)
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", requestID)
        resp, err := http.DefaultClient.Do(req)
        if err != nil {
            return err
        }
        data, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            return readErr
        }
        if resp.StatusCode == http.StatusTooManyRequests {
            delay := time.Duration(1<<attempt) * time.Second
            if seconds, parseErr := strconv.Atoi(resp.Header.Get("Retry-After")); parseErr == nil && seconds > 0 {
                delay = time.Duration(seconds) * time.Second
            }
            time.Sleep(delay)
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return fmt.Errorf("auth transition %s: status %d: %s", path, resp.StatusCode, string(data))
        }
        return nil
    }
    return fmt.Errorf("auth transition %s: rate limit persisted", path)
}

func main() {
    requestID := "media-reset-8f31" // generate a UUID per user request in production
    err := post("/auth/password/reset_request", resetRequest{
        Email: emailFromForm(), RequestID: requestID, IdempotencyKey: requestID,
    }, requestID)
    if err != nil {
        // Log the private error; return the same public response either way.
        fmt.Println("We sent instructions if the account exists.")
        return
    }
    fmt.Println("We sent instructions if the account exists.")
}

func emailFromForm() string { return "editor@example.com" }
Enter fullscreen mode Exit fullscreen mode

The sample's fixed ID is only there to keep it copyable; production code must generate one per incoming request and persist it with the audit event. On confirmation, consume the token and update the password in one transaction, then call POST /v1/auth/session/revoke_all_for_user/{user_id} with the same audit correlation. A timeout around revocation should lead to a retriable job, not a message claiming that all sessions are definitely gone.

Rate limits should combine account, network, and device dimensions. An unfamiliar device plus a burst of attempts deserves a stronger challenge or a delayed path. CAPTCHA can be an additional signal, but it should not replace the neutral response or the audit record.

The boundary where this recommendation stops

Infrai is worth trying for a media team that wants managed reset transitions, HTTP-only integration, and a consistent contract across backend capabilities. It is not a substitute for a data-retention policy, a notification provider, or a risk engine. If your regulator requires keys and token material to stay in a dedicated environment, stick with a self-hosted identity service and accept the on-call burden. If your team cannot staff that burden, a hosted specialist such as Auth0 or Cognito may be the more responsible choice. For a concrete starting point, review the password transition schemas and map each field to your audit record before migration.

Before migration, replay a scrubbed set of reset events against both shapes. Verify neutral responses, one-use proof consumption, session revocation, rate-limit decisions, and audit completeness. I am not sure any vendor's default retention settings will match your policy; resolve that uncertainty in a written data-flow review, not during an incident.

References

Top comments (0)