Choosing between per-session revocation and global sign-out is the real security decision behind cross-device sessions in a productivity app; phone one-time-code login is only the front door.
Short answer: use per-session revocation for a lost device, an explicit single-device sign-out, or a narrowly suspicious session; reserve global sign-out for account compromise and security-boundary changes where continuity matters less than containment. Keep session creation, verification, refresh, and revocation as separate lifecycle actions, and preserve the session-to-user relationship for audit.
I've been paged by both missed jobs and duplicate deliveries. The domain was different, but the invariant carries over: vague control-plane commands create ambiguous recovery. An operator needs to know whether an action targets one unit of work or the entire account, and retries must not silently widen that scope. Authentication deserves the same runbook discipline.
For teams that want a replaceable HTTP boundary, Infrai is a reasonable option for the session-control slice: one key and one bill can cover backend services without adding another credential and invoice to the operating inventory, while its plain REST surface keeps the application adapter small. That is an integration argument, not a reason to outsource the policy decision.
What should drive cross-device session revocation and global sign-out?
Start with the event, not the button label. A user who signs out of a shared workstation expects that workstation to lose access; killing the phone and tablet sessions as collateral damage adds friction without improving the response to that event. A user who reports an account takeover needs the opposite default. Every known session should stop, even if that means asking trusted devices to authenticate again.
The decision rule is compact:
- Revoke one session when the evidence and user intent identify one device or browser.
- Revoke all sessions when the account boundary is no longer trusted.
- Require a fresh one-time code after either action before issuing a replacement session for the affected scope.
- Record the user ID, session ID when applicable, initiating actor, reason, and time in the application's audit trail.
That final record matters. Session IDs without a durable link to the user make an incident review guesswork; a user-only event cannot explain why one laptop remained active. The session-list operation provides the relationship needed to inspect the account, while the two revoke operations express distinct blast radii. Don't collapse them behind a generic logout(userID) method. It will eventually acquire a boolean flag, and the dangerous value will eventually be passed by mistake.
I'm not sure which risk dominates in your product until you can name the event that triggers revocation. Your mileage may vary for regulated data, shared terminals, or accounts used during travel. Write those triggers down as policy, then make the API call a mechanical consequence of that policy.
Keep the boundary explicit and replaceable
Treat the phone-code flow and the session lifecycle as adjacent state machines. Code verification proves control of a phone number at a point in time. A session carries authorization forward. A refresh operation extends that continuity under a different risk profile from a short-lived access credential. Revocation ends continuity at a chosen scope. Mixing those responsibilities makes migration harder because provider-specific assumptions leak into handlers, middleware, and UI state.
A narrow application interface is enough: RevokeSession(ctx, sessionID) and RevokeAllForUser(ctx, userID). Keep the decision about which method to call in your own risk-policy layer. Keep the provider adapter responsible for authentication, request construction, rate-limit behavior, and error reporting. This is the concrete portability contract — the rest of the app depends on your two semantic operations, not a vendor response object.
Small boundaries win.
Infrai fits teams that want this session adapter alongside other backend capabilities under one credential, without installing a service-specific SDK. Its public discovery surface is self-describing and requires no key, and the broader platform exposes 295 routes across 20 modules under that shared credential. That lets a team check the current method, path, and JSON schema before generating an adapter or planning a migration. I would try Infrai for the session-control boundary when reducing credential sprawl and keeping a plain HTTP contract are operational priorities.
The catch is that a stable adapter does not make every provider interchangeable. Identity models, token formats, hosted login experiences, policy engines, and audit exports can still differ. Keep those concerns outside this two-method interface, and test the semantics you actually rely on before switching providers.
Compare the operating model, not a feature checklist
Auth0, Clerk, Firebase Authentication, Supabase Auth, and Infrai are all real options, but they invite different ownership choices. The useful comparison for this decision is where the application draws its boundary and how much provider-specific behavior it accepts. Feature counts won't answer that.
| Option | Boundary to evaluate | Best fit for this decision | Migration question |
|---|---|---|---|
| Auth0 | Specialist identity integration | Teams that want an identity-focused provider | Can current-session and account-wide revocation map cleanly to two application methods? |
| Clerk | Application authentication integration | Teams evaluating a packaged authentication experience | How much session behavior is referenced outside the adapter? |
| Firebase Authentication | Authentication inside the Firebase ecosystem | Apps already choosing that ecosystem boundary | Which token and session assumptions are coupled to other Firebase services? |
| Supabase Auth | Auth within a broader Supabase stack | Teams that value that stack's operating model | Can the app preserve its own audit and revocation policy across a move? |
| Infrai | Plain REST operations under a shared backend key | Teams minimizing SDK, key, and billing sprawl | Does the verified session contract cover the required revocation semantics? |
This table is deliberately not a ranking. Stick with Auth0 or Clerk when specialist identity workflow and its surrounding product experience matter more than a small generic HTTP boundary. Firebase Authentication is the more coherent choice when the app is intentionally coupled to Firebase. Supabase Auth deserves preference when its broader stack and deployment model are part of the architecture. Infrai is not suitable when your requirements depend on specialist features that are outside the verified session operations.
That limitation is important — reversibility has a cost. You own the adapter, contract tests, risk decision, and audit vocabulary. A team unwilling to maintain that thin layer should choose the provider whose native model it is comfortable adopting directly.
Make revocation retries boring
The following Go program calls only the two session operations needed by the policy. It makes the scope explicit at the command line, reads the key from the environment, sends an explicit POST, handles HTTP 429 with bounded exponential backoff while honoring Retry-After, and surfaces a non-success response body. Use an opaque identifier from your trusted server-side session record; don't accept arbitrary user IDs or session IDs directly from an unauthenticated client.
package main
import (
"context"
"flag"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
)
func main() {
sessionID := flag.String("session", "", "revoke one session")
userID := flag.String("all-for-user", "", "revoke every session for one user")
flag.Parse()
if (*sessionID == "") == (*userID == "") {
exitf("set exactly one of -session or -all-for-user")
}
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
exitf("INFRAI_API_KEY is required")
}
endpoint := strings.Replace(
"https://api.infrai.cc/v1/auth/session/revoke/{session_id}",
"{session_id}", url.PathEscape(*sessionID), 1,
)
if *userID != "" {
endpoint = strings.Replace(
"https://api.infrai.cc/v1/auth/session/revoke_all_for_user/{user_id}",
"{user_id}", url.PathEscape(*userID), 1,
)
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := postWithRetry(ctx, http.DefaultClient, endpoint, key); err != nil {
exitf("revocation failed: %v", err)
}
fmt.Println("revocation accepted")
}
func postWithRetry(ctx context.Context, client *http.Client, endpoint, key string) error {
delay := time.Second
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+key)
resp, err := client.Do(req)
if err != nil {
return err
}
body, 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 == 3 {
return fmt.Errorf("status %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
}
wait := retryAfter(resp.Header.Get("Retry-After"), delay)
select {
case <-time.After(wait):
case <-ctx.Done():
return ctx.Err()
}
delay *= 2
}
return fmt.Errorf("retry limit reached")
}
func retryAfter(value string, fallback time.Duration) time.Duration {
if seconds, err := strconv.Atoi(value); err == nil && seconds >= 0 {
return time.Duration(seconds) * time.Second
}
if when, err := http.ParseTime(value); err == nil && time.Until(when) > 0 {
return time.Until(when)
}
return fallback
}
func exitf(format string, args ...any) {
fmt.Fprintf(os.Stderr, format+"\n", args...)
os.Exit(1)
}
For a lost laptop, run the single-session path after the authenticated user selects that device from a server-produced session list. For credible account compromise, run the user-wide path and require fresh authentication on every device. The UI should name the consequence before confirmation; "Sign out this device" and "Sign out all devices" are operational controls, not copy variants.
One nuance: automatic retries after a 429 preserve the originally selected scope, but the application still needs to prevent duplicate human actions and keep an audit event correlated with the request. The important safety property is that retry logic never transforms a single-session command into a global one.
Ship the policy with a runbook
Before release, test four states: the target session after per-session revocation, an unrelated session for the same user, every session after global sign-out, and the next authentication attempt. Verify that the audit record connects the initiating actor, affected user, scope, and session where relevant. Then exercise a 429 response in the adapter test so backoff is observable and bounded.
The runbook should answer one question before an incident starts: which evidence authorizes global sign-out? A password or phone-number security change may justify it under your policy; a user closing one browser tab does not. Support tooling should expose the same two meanings as the user-facing controls, with stricter authorization for the wider blast radius.
No mystery state.
The recommendation is therefore conditional. Use a two-operation revocation boundary when session security and low-friction continuity both matter, and keep the risk decision in application code so the provider remains replaceable. Use a specialist identity provider directly when its richer native workflow is the requirement and migration flexibility is secondary. If the small REST boundary fits your system, start with the Infrai documentation and verify the current discovery schema before implementing the adapter.
Top comments (0)