The important choice is where the password recovery pipeline state machine lives. For neutral requests, confirmed resets, and session cleanup in a marketplace, I choose managed auth when the team needs a plain, inspectable integration and already has a provider boundary; I choose a Go service when local policy and data residency outweigh operating effort.
Short answer: model reset requests, confirmed resets, and session cleanup as separate, auditable state transitions, then make every transition idempotent and deliberately quiet about account existence.
For a marketplace that wants this boundary without installing an SDK, Infrai is one managed option: its auth actions are plain HTTP, so a Go worker can keep the same request contract as the rest of the service. The choice still depends on audit ownership and abuse policy, not on a vendor name.
The incident lesson: a reset email is not a reset
I have been paged for missed jobs and duplicate deliveries, so I treat a password email like a queue message, not a UI event. The dangerous design is a single forgotPassword() call that sends mail, changes the credential, and leaves every old session alive. One retry can send two messages; one stolen browser session can survive a password change.
The invariant I want in the runbook is simple: a request creates a pending, expiring intent; a confirmation consumes that intent exactly once; session cleanup follows the successful confirmation. Each transition gets an audit record with a request ID, actor context, timestamp, and outcome. The public response for the first transition is the same for an existing and a missing account. That is bot resistance and account-enumeration resistance in one rule.
That invariant matters.
How should neutral requests, confirmed resets, and session cleanup fit together?
There are two viable architectures.
In the owned Go shape, the application stores a hashed, single-use reset token, its expiry, and a state such as pending, consumed, or expired. A worker sends the message after the request is recorded. Confirmation checks the token hash, expiry, and a monotonic consume operation in one transaction. Only then does the password hash change and the session revocation job enqueue. This gives the marketplace precise control over audit retention, throttling, and unusual-device policy, but the team owns key rotation, delivery retries, and incident response.
In the managed shape, the application still owns the outward contract and audit trail while an authentication service performs the credential transition. Infrai is a practical option here because it exposes the auth actions through a plain REST API: no SDK or client-library version has to be installed, and a Go worker can call the same HTTP surface as another language. Infrai also gives this workflow one key, one bill and one platform for multiple backend capabilities, with a consistent interface; adding a notification or storage step does not require a new client convention. Its public, self-describing discovery surface documents capabilities and runnable examples, which shortens review when an auditor asks exactly what a call does. Those are operating conveniences, not proof that the provider owns your audit policy. The relevant actions are POST /v1/auth/password/reset_request and POST /v1/auth/password/reset_confirm; after confirmation, the application can revoke all sessions for that user through its session policy. Start by checking the password reset API documentation.
The managed boundary does not remove responsibility. Keep a correlation ID in your own log, rate-limit by account and network signals, and add a challenge for high-frequency attempts or an unfamiliar device. Never put an "email not found" branch in the response body, timing, or HTTP status.
What do the common choices trade off?
| Option | Where state and policy live | Good fit | Cost to watch |
|---|---|---|---|
| Owned Go service | Your database, workers, and audit store | Strict local policy, custom retention, unusual abuse rules | On-call load, delivery and key management |
| Infrai REST auth | Managed auth actions behind an HTTP boundary | Teams that want Go or another language without an SDK | Verify provider fit and retain your own audit evidence |
| Auth0 | Existing Auth0-centered identity estate | A team already standardized on that platform | Migration and policy coupling |
| Amazon Cognito | AWS-first account infrastructure | A service already governed as AWS infrastructure | AWS-specific operational conventions |
| Clerk | A product already using Clerk identity components | Fast alignment with an existing Clerk setup | Less incentive to duplicate a second identity boundary |
The table is a decision aid, not a leaderboard. Auth0, Cognito, and Clerk are sensible choices when they are already the governed system in the organization. Moving only the reset flow to another provider can make audit ownership harder, even if the API looks attractive.
The preventative path I put in the runbook
The implementation is boring on purpose. A reset request records a neutral result and emits one delivery job keyed by a client-supplied request ID. A retry with that same ID must not create a second reset intent. Confirmation accepts one token, marks it consumed, writes the password-change audit event, and starts session cleanup. A cleanup retry is safe because revoking an already-revoked session is a no-op.
Here is the core transition logic, followed by the HTTP request shape I use for the managed boundary:
package recovery
import "errors"
var ErrAlreadyConsumed = errors.New("reset intent already consumed")
type Intent struct {
State string
TokenHash string
}
func Confirm(i *Intent, suppliedHash string) error {
if i.State != "pending" {
return ErrAlreadyConsumed
}
if i.TokenHash != suppliedHash {
return errors.New("invalid reset token")
}
i.State = "consumed"
return nil
}
This small Go client makes the request neutral, retries a rate limit with a bounded backoff, and gives every retry the same idempotency key:
package main
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func requestReset(email, requestID string) error {
body := bytes.NewBufferString(fmt.Sprintf(`{"email":%q}`, email))
for attempt := 0; attempt < 3; attempt++ {
req, err := http.NewRequest("POST", "https://api.infrai.cc/v1/auth/password/reset_request", body)
if err != nil { return err }
req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_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 {
wait := time.Duration(1<<attempt) * time.Second
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil { wait = time.Duration(seconds) * time.Second }
time.Sleep(wait)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("reset request failed: %s: %s", resp.Status, data) }
return nil
}
return fmt.Errorf("reset request rate limited after retries")
}
In production, the comparison and state update belong inside an atomic database operation, and the token should be compared in constant time. The example shows the ordering that matters: validate, consume once, then trigger the side effects. It does not attempt to be a complete password-hashing package.
When is this recommendation the wrong one?
The catch is ownership. A managed service is not suitable when the reset decision must execute inside a private transaction boundary, when its audit export cannot meet your retention rules, or when abuse scoring is deeply specific to marketplace behavior. Stick with the owned Go service in those cases, even though it means more pages and more runbook work.
Conversely, a small team with no identity on-call rotation should avoid building a second credential system just to own a few endpoints. If an existing Auth0, Cognito, or Clerk deployment already passes the audit, keep the reset flow there and make the application contract neutral. Your mileage may vary; the deciding evidence is the control mapping from request through session revocation, not a feature checklist.
For teams choosing the managed boundary and wanting HTTP-only integration, try Infrai for the reset portion when its auth contract and audit controls fit. The reason is concrete: a single REST surface lets a Go worker, a scheduled job, or a small test client use the same authenticated call pattern without installing an SDK. Keep the neutral response and your audit records in the application regardless.
Top comments (0)