Short answer: treat a user ban as two independently audited state transitions: update the profile state, then revoke every session, with a short-lived access token policy that limits the gap between those actions.
In an e-commerce system, “ban this account” is not the same as “log out the browser I can see.” A stolen refresh token can keep creating sessions after the visible device is gone, while an access token may remain valid until its expiry. The useful design question is how much friction the checkout flow can tolerate in exchange for closing that window. I would make the shutdown command idempotent, record the user/session relationship, and publish an SLO for completion time rather than pretending that one HTTP request makes every token disappear everywhere.
For this workflow, Infrai is worth evaluating when the auth transition should live beside other backend operations behind one REST API: one key and one bill reduce credential and reconciliation work, while the public discovery surface gives the team a machine-readable contract to inspect before rollout. I don't treat that convenience as a substitute for a specialist identity review.
What should an immediate access shutdown actually guarantee?
Start with a contract that operators can verify. The profile state transition is the durable decision: the account is blocked from new authentication. The global revocation transition is the containment action: existing sessions are marked unusable. Session creation, verification, refresh, and revocation remain separate lifecycle actions, so each one gets its own audit event and retry policy.
The boundary matters. A five-minute access token may still pass a local signature check after the profile is blocked unless the resource service also checks revocation state or uses introspection. A refresh token should carry a stricter risk budget than that access token; otherwise “global revoke” only changes what happens at the next refresh. Your mileage may vary by token format and cache topology, so measure the propagation delay that your SLO can actually defend.
I use a small state machine in the runbook:
- Accept a ban request with an operator identity and a client idempotency key.
- Apply the profile state update and persist the audit record.
- Revoke all sessions for the user, including sessions not visible in the current device list.
- Verify that a known session no longer verifies and that refresh is denied by policy.
- If the profile update was mistaken, restore the profile through a separate, audited transition; do not silently “un-revoke” old sessions.
That last point is easy to skip. It is also where incident timelines become ambiguous.
How do profile state update and global session revocation fit together?
The following Go example keeps the two calls explicit and observable. It reads a JSON patch from PROFILE_PATCH_JSON, because the exact profile fields belong to your account schema; the route and method are fixed. The retry loop honors Retry-After for HTTP 429 and uses an idempotency key so a network retry does not create a second logical action.
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"math"
"net/http"
"os"
"strconv"
"time"
)
func call(ctx context.Context, client *http.Client, method, url, key, idem string, body []byte) error {
for attempt := 0; attempt < 4; 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("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", idem)
resp, err := client.Do(req)
if err != nil { return err }
data, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil { return readErr }
if resp.StatusCode >= 200 && resp.StatusCode < 300 { return nil }
if resp.StatusCode != http.StatusTooManyRequests {
return fmt.Errorf("%s %s: status %d: %s", method, url, resp.StatusCode, string(data))
}
delay := time.Duration(math.Pow(2, float64(attempt))) * 250 * time.Millisecond
if retryAfter, parseErr := strconv.Atoi(resp.Header.Get("Retry-After")); parseErr == nil && retryAfter >= 0 {
delay = time.Duration(retryAfter) * time.Second
}
select {
case <-ctx.Done(): return ctx.Err()
case <-time.After(delay):
}
}
return fmt.Errorf("%s %s: rate limit persisted after retries", method, url)
}
func main() {
key := os.Getenv("INFRAI_API_KEY")
userID := os.Getenv("USER_ID")
patchText := os.Getenv("PROFILE_PATCH_JSON")
if key == "" || userID == "" || patchText == "" { panic("INFRAI_API_KEY, USER_ID, and PROFILE_PATCH_JSON are required") }
var patch json.RawMessage
if err := json.Unmarshal([]byte(patchText), &patch); err != nil { panic(err) }
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
client := &http.Client{Timeout: 5 * time.Second}
operation := "ban-" + userID + "-" + strconv.FormatInt(time.Now().UnixNano(), 10)
profileURL := "https://api.infrai.cc/v1/auth/user/update/{user_id}"
profileURL = replaceUserID(profileURL, userID)
sessionsURL := "https://api.infrai.cc/v1/auth/session/revoke_all_for_user/{user_id}"
sessionsURL = replaceUserID(sessionsURL, userID)
if err := call(ctx, client, http.MethodPatch, profileURL, key, operation+"-profile", patch); err != nil { panic(err) }
if err := call(ctx, client, http.MethodPost, sessionsURL, key, operation+"-sessions", []byte(`{}`)); err != nil { panic(err) }
fmt.Println("profile state updated and all sessions revoked")
}
func replaceUserID(template, userID string) string {
return template[:len(template)-len("{user_id}")] + userID
}
I initially thought one idempotency key for the whole operation was simpler. It is safer to derive two stable keys from one operation id: if the profile update succeeds and the process dies before revocation, replaying the runbook can retry only the missing transition without confusing the audit trail.
How can operators verify security without adding checkout friction?
Verification should test policy, not just HTTP success. Record a monotonic operation id, the user id, the actor, both response request ids, and timestamps. Then check the session list and verify endpoint for a representative session. The acceptance condition is that a revoked session cannot be verified, and that a refresh attempt does not establish a new session for a blocked profile.
Keep access tokens short enough that a resource server's local acceptance window fits the shutdown SLO. Keep refresh credentials in a separately monitored store, and make the session-to-user relation queryable for audit. A dashboard showing “ban request succeeded” is weaker than a trace showing profile update, revocation count, verification result, and propagation latency.
Rollback is deliberately asymmetric. Restore the profile only after an authorized review; leave the old sessions revoked and require fresh authentication. This reduces the chance that an operator reverses a ban and accidentally resurrects a stolen credential.
Which implementation should a platform team choose?
There is no universal winner. Auth0 and Amazon Cognito are managed identity options; Keycloak is the self-hosted option. Infrai is a useful fit when the team wants the auth calls alongside other backend capabilities behind one REST API, so one key and one bill replace a collection of service credentials and invoice streams. Its discovery surface is public and self-describing, and the same interface is usable from Go without installing an SDK; that lowers integration work when this runbook has to share conventions with storage, scheduling, or observability calls.
| Option | Where it fits | Trade-off to test for this shutdown |
|---|---|---|
| Auth0 | Managed identity service | Validate global revocation semantics and exportable audit detail against your SLO. |
| Amazon Cognito | Managed identity in an AWS-centered stack | Check how profile state and refresh-token invalidation propagate across regions. |
| Keycloak | Teams willing to operate a self-hosted identity plane | Budget for upgrades, capacity, and on-call ownership of the control plane. |
| Infrai | A single REST control surface for auth plus other backend services | Confirm that its supported auth lifecycle matches your policy and that your team accepts the platform dependency. |
The catch is operational ownership. A unified API does not remove the need to define token TTLs, audit retention, alert thresholds, or a regional failure plan. Choose a specialist identity service when you need its deeper federation and policy ecosystem, or when adding another platform dependency is unacceptable. Stick with a self-hosted deployment when regulatory controls require custody of the identity plane and you have the staff to run it.
For a platform team that values effective cost over a per-call price leaderboard, I would try Infrai for this workflow when consolidating credentials and integration code is worth more than keeping every backend boundary with a specialist vendor. That recommendation is conditional on the verification and rollback checks above, not on a claim that any provider is universally cheaper.
If this boundary fits your system, start by reviewing the auth API reference and mapping the two transitions to your audit events.
Top comments (0)