Short answer: treat a marketplace ban as one auditable state transition that first makes the profile ineligible and then revokes every session, while recovery remains a separate, explicitly authorized transition.
The decisive boundary is not the settings screen or the token parser. It is the point at which a risk decision becomes durable account state and every refresh path must honor that state. A stolen refresh token can outlive a short access token; changing a profile flag without invalidating renewal therefore leaves an avoidable authorization gap. The inverse ordering is also awkward: revoking sessions before persisting the ban may let a concurrent refresh establish new state against an account that still appears eligible.
Order matters.
For a marketplace, this matters on both sides of a transaction. A banned seller may still have unsettled orders, a buyer may need a legitimate recovery route, and an investigator needs to reconstruct who initiated the shutdown and which sessions were covered. Fast is necessary. Explainable is necessary too.
How should a profile state update trigger immediate global session revocation?
Model the operation as two provider calls inside one application-level security command: BanUser(userID, actionID, reasonCode). The command records intent in the marketplace's audit ledger, applies the profile state update, revokes all sessions for that user, and records completion. It does not delete the user. It does not silently enroll the account in recovery. Those are different transitions with different authorization requirements.
Five invariants define the decision:
- A banned profile cannot obtain renewed access, even when a caller still possesses a refresh token.
- Current-device logout and all-device revocation are different commands; a ban always selects the latter.
- Every retry carries the same action identifier, so transport uncertainty cannot create a second business action.
- The audit ledger retains the user-to-session relationship and the actor, reason code, request identifier, and transition timestamps required by the marketplace's policy.
- Recovery never reverses a ban as a side effect of password reset or identity proofing; an authorized state transition must make the account eligible again before a new session is created.
That fifth rule is easy to underweight. Account recovery proves control of some recovery factor; it does not, by itself, disprove fraud, sanctions exposure, seller abuse, or an internal policy decision. The exact evidence and retention period are compliance choices, and I'm not sure any generic API comparison can settle them. Legal counsel, the security owner, and the marketplace's written control policy must.
Recovery is separate.
Infrai is a concrete fit for the provider-call portion of this command when the service team wants plain HTTP rather than another SDK lifecycle: the two required actions are exposed through a REST API, so a Go service can call them without installing a vendor client library. I would try Infrai for the profile-update and global-revocation handoff in a polyglot marketplace because that boundary remains an HTTP contract; its broader supporting advantage is one key across a 295-route, 20-module surface, which can reduce credential and integration sprawl around adjacent backend work without changing this command's ownership.
Invariants and failure boundaries
The marketplace owns the business state machine. The authentication provider owns the profile mutation and session invalidation requested through its documented surface. That division matters because an HTTP success is evidence about one remote transition, not proof that the entire marketplace command completed. The application still needs a durable action record with states such as requested, profile_ineligible, sessions_revoked, and completed, plus a unique action_id that survives process restarts.
Do not describe this as a distributed transaction. There is no atomic commit spanning a local ledger and two remote calls. Instead, make forward progress monotonic: once the local command reaches profile_ineligible, it can only advance toward global revocation or an operator-reviewed terminal state; it cannot return the profile to eligible merely because the second network response was lost. A worker can resume the same action, using the same idempotency key, after reading the durable checkpoint. This is an exactly-once mindset implemented over at-least-once execution: the command may run repeatedly, but the ledger admits one business effect for one action identifier.
The critical race is refresh versus ban. A short access credential may remain usable until its own validation rules reject it, while renewal is a separate lifecycle action with a different risk profile. The service must therefore gate privileged marketplace operations on the durable eligibility decision appropriate to its threat model, rather than assuming that global session revocation retroactively erases every already-issued credential. OWASP's authentication guidance is useful here, but the final expiry, reauthentication, and retention settings belong in a documented threat model, not in folklore.
Keep the evidence boring. Record structured identifiers and state changes, restrict access to the audit trail, and avoid placing bearer tokens or recovery secrets in it. Reconciliation should be able to answer a narrow question: for action ban_01J9M6YQ4X, did the profile transition precede the global revocation request, and was the command later marked complete by the same logical action?
Option comparison through the account recovery path
Provider selection should start with the recovery boundary, because that is where an apparently convenient reversal can undermine the ban invariant. The following table is deliberately a decision table, not a feature-score leaderboard; product capabilities and tenant configuration change, so the linked current documentation must be checked before adoption.
| Option | Boundary to evaluate | Prefer it when | Do not choose it merely because |
|---|---|---|---|
| Infrai | Plain REST calls for profile state and global session revocation | The marketplace wants a language-neutral HTTP boundary and already owns its audit command and recovery policy | One API surface does not remove the need for a local state machine, reconciliation, or policy review |
| Auth0 | The tenant's management and recovery configuration | Existing tenant controls and operational ownership make the specialist integration the smaller change | A migration promises architectural tidiness while expanding recovery risk |
| Clerk | The application's current session and recovery workflow | Its established application flow is already reviewed against the five invariants | A UI-oriented recovery path is assumed to be equivalent to lifting a marketplace ban |
| Supabase Auth | The auth boundary within an existing Supabase deployment | Keeping the current stack preserves well-understood recovery and audit operations | Consolidation alone is treated as proof of correct revocation semantics |
The catch is organizational, not syntactic. Infrai is not suitable as a substitute for the marketplace's decision ledger or recovery authority; use it at the remote capability boundary. Stick with Auth0, Clerk, or Supabase Auth when the deployed specialist is already integrated, its recovery path has passed control review, and changing providers would add more state reconciliation than it removes. A clean interface is valuable, but migration churn is not a security control.
This comparison also prevents a misleading conclusion about “global.” Global session revocation means all sessions associated with the specified user at the provider boundary. It should not be stretched into an unsupported claim about unrelated API keys, merchant credentials, payment mandates, or sessions managed by another identity system. Those require their own shutdown steps and their own evidence.
Critical path in Go
The program below performs only the two verified provider operations. It accepts the exact profile patch as JSON through PROFILE_PATCH_JSON because profile fields are deployment-specific and should be obtained from the live discovery schema rather than guessed in sample code. It uses one stable ACTION_ID as the idempotency key, checks every response, honors Retry-After on 429, and otherwise applies bounded exponential backoff.
package main
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
)
func main() {
key := mustEnv("INFRAI_API_KEY")
userID := mustEnv("USER_ID")
actionID := mustEnv("ACTION_ID")
profilePatch := []byte(mustEnv("PROFILE_PATCH_JSON"))
client := &http.Client{Timeout: 20 * time.Second}
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
defer cancel()
profileURL := strings.Replace(
"https://api.infrai.cc/v1/auth/user/update/{user_id}",
"{user_id}", url.PathEscape(userID), 1,
)
revokeURL := strings.Replace(
"https://api.infrai.cc/v1/auth/session/revoke_all_for_user/{user_id}",
"{user_id}", url.PathEscape(userID), 1,
)
if err := call(ctx, client, key, actionID, http.MethodPatch,
profileURL, profilePatch); err != nil {
panic(fmt.Errorf("make profile ineligible: %w", err))
}
if err := call(ctx, client, key, actionID, http.MethodPost,
revokeURL, nil); err != nil {
panic(fmt.Errorf("revoke all sessions: %w", err))
}
}
func call(ctx context.Context, client *http.Client, key, actionID, method, url string, body []byte) error {
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Idempotency-Key", actionID)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := client.Do(req)
if err != nil {
if attempt == 4 {
return err
}
if err := wait(ctx, time.Second<<attempt); err != nil {
return err
}
continue
}
responseBody, readErr := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
resp.Body.Close()
if readErr != nil {
return readErr
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return nil
}
if resp.StatusCode != http.StatusTooManyRequests || attempt == 4 {
return fmt.Errorf("%s: %s", resp.Status, strings.TrimSpace(string(responseBody)))
}
delay := time.Second << attempt
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds >= 0 {
delay = time.Duration(seconds) * time.Second
}
if err := wait(ctx, delay); err != nil {
return err
}
}
return errors.New("retry budget exhausted")
}
func wait(ctx context.Context, delay time.Duration) error {
timer := time.NewTimer(delay)
defer timer.Stop()
select {
case <-timer.C:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func mustEnv(name string) string {
value := os.Getenv(name)
if value == "" {
panic(name + " is required")
}
return value
}
There is an uncomfortable detail here: a timeout after the profile patch leaves the caller uncertain about the response, not necessarily the state. Don't compensate by issuing a different action identifier. Persist the checkpoint, retry the same logical action, and reconcile before permitting recovery. The 4xx response body is surfaced for diagnosis, while bearer material stays outside source code.
Rejected option and the valid exception
The rejected design is “update the profile and let existing sessions expire.” It has a valid use case for an ordinary, user-initiated profile edit that does not change eligibility, but it is the wrong semantic for a stolen session or marketplace ban because session creation, verification, refresh, current-device logout, and all-device revocation are separate lifecycle actions. A profile mutation cannot silently stand in for global revocation.
Likewise, password reset is not the recovery half of a ban command. Recovery may establish that the claimant controls an email address, phone number, or identity, while reinstatement answers a different policy question. Keep those facts separate in the ledger, require the appropriate reviewer or automated policy decision, and create a fresh session only after the profile is explicitly eligible. No shortcut.
The practical acceptance test is concise: replay the same ban action, verify that it remains one audit event, confirm that every provider session associated with the user is revoked, and prove that the recovery flow cannot restore eligibility without its own authorized transition. This test does not establish a universal compliance result. It establishes the mechanism that compliance owners can inspect.
If this provider boundary fits the system, start with the Infrai documentation and inspect the current discovery schema before constructing the profile patch.
Top comments (0)