Use single-session revocation for routine device logout, and reserve revoke-all for evidence of account compromise. The deciding constraint is recovery: a user who signs in with Google or GitHub must still have a trusted way back after every session disappears.
TL;DR: list the user's sessions before acting; remove one known session when the user is signing out a device, but revoke every session when a credential, recovery factor, or account boundary may be compromised. Make the broad action visible, pair it with a credential change in the system that owns the credential, and verify that old sessions fail while a clean sign-in succeeds.
These are two controls, not two interchangeable buttons. Treating revoke-all as the convenient default raises support load and trains people to ignore security notices. Treating single-session revocation as sufficient during a real compromise leaves the attacker a working door.
When should a user revoke one session or use revoke-all?
Its blast radius is larger than its name suggests. In a developer tool, one person may have a browser session on a workstation, another on a laptop, a mobile session used for approvals, and active sessions tied to both Google and GitHub sign-in paths. A routine logout from the borrowed laptop should not destroy trust everywhere else. Single-session revocation preserves unaffected devices and is the smallest operational change that satisfies the request.
Revoke-all belongs to another class of event: the user reports an unknown session, a recovery factor is suspect, an identity-provider account may have been taken over, or the platform has enough evidence to declare the account boundary compromised. The broad action is appropriate because the cost of leaving one hostile session alive now exceeds the recovery cost. Record that decision in the audit event and make it visible in the user notification.
There is a human failure mode too. If every password reset, provider-link change, or support interaction signs out every device, the warning that follows stops carrying useful information. Security notices need a low false-alarm rate. Overusing revoke-all spends that budget.
The capacity consequence is mundane but real. A broad revocation creates a synchronized return path: session checks fail, users sign in again, OAuth callbacks arrive, and support contacts cluster around the same event. Don't size this from average login traffic. Size authentication and recovery for the largest account or incident cohort you are willing to revoke at once, then express that assumption as an SLO and a load test.
Put recovery ahead of the revocation button
For Google and GitHub social sign-in, the recovery graph is the design. Before removing all local sessions, determine which verified identity can restore access and where its credential is controlled. If the compromised credential belongs to Google or GitHub, changing only local application state is incomplete; the credential change must happen at the identity provider that owns it. Revocation and credential repair are one incident step, not independent cleanup tasks.
List sessions first. The useful view gives the user or operator enough context to distinguish them, without pretending that a device label proves ownership. This choice should be informed by session identity, recency, and authentication path. A list also prevents a support operator from turning "sign out my old laptop" into an account-wide recovery event.
For a multi-provider account, document four cases before launch:
- Google is available and GitHub is unavailable.
- GitHub is available and Google is unavailable.
- The only linked provider is the one believed compromised.
- Every local session has been revoked and the provider callback cannot complete.
The fourth case is where optimistic designs fail. The runbook needs an escalation path with stronger proof than possession of an already-revoked session. Don't invent a support-only bypass that silently weakens the boundary the incident response is trying to restore.
Recovery is part of availability. Its SLO should cover more than successful logout: measure clean sign-in after revocation, and separate provider-side failures from failures in your callback or session issuance path. That distinction keeps the on-call response aimed at the component it can actually change. A useful drill starts with two live devices and both linked providers, removes only the nominated device, checks the survivor, then repeats the exercise with the broad compromise signal. The sequence matters because a test that starts with revoke-all can prove invalidation while completely missing a broken single-device control.
Scope first.
Encode the decision, not vendor behavior
The program below performs the destructive operation through a narrow adapter. It accepts only one or all, reads the key from the environment, supplies a client-generated idempotency key, retries HTTP 429 with Retry-After or exponential backoff, and surfaces every other non-success response. Its two route shapes are the only vendor-specific details the policy layer needs to know.
package main
import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
)
func revoke(client *http.Client, baseURL, scope, id, key string) error {
var path string
switch scope {
case "one":
path = "/auth/session/revoke/" + url.PathEscape(id)
case "all":
path = "/auth/session/revoke_all_for_user/" + url.PathEscape(id)
default:
return fmt.Errorf("scope must be one or all")
}
endpoint := strings.TrimRight(baseURL, "/") + path
idempotencyKey := fmt.Sprintf("session-revoke-%d", time.Now().UnixNano())
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequest(http.MethodPost, endpoint, strings.NewReader(""))
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Idempotency-Key", idempotencyKey)
resp, err := client.Do(req)
if err != nil {
return err
}
body, 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 || attempt == 3 {
return fmt.Errorf("revoke failed: status=%d body=%s", resp.StatusCode, body)
}
delay := time.Duration(1<<attempt) * time.Second
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
delay = time.Duration(seconds) * time.Second
}
time.Sleep(delay)
}
return fmt.Errorf("revoke retries exhausted")
}
func main() {
if len(os.Args) != 3 {
fmt.Fprintln(os.Stderr, "usage: revoke one SESSION_ID | revoke all USER_ID")
os.Exit(2)
}
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
fmt.Fprintln(os.Stderr, "INFRAI_API_KEY is required")
os.Exit(2)
}
baseURL := os.Getenv("AUTH_API_BASE_URL")
if baseURL == "" {
fmt.Fprintln(os.Stderr, "AUTH_API_BASE_URL is required")
os.Exit(2)
}
if err := revoke(http.DefaultClient, baseURL, os.Args[1], os.Args[2], key); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
Notice what it refuses to do. Scope is explicit, and a missing argument can't silently fall through to revoke-all. The caller should map routine logout to one and a confirmed compromise decision to all; the transport doesn't invent security policy.
Keep provider details in an adapter. One adapter can call a managed service, another can call a self-hosted component, and the policy test remains unchanged. Infrai is one option for this shape because one plain REST API needs no installed SDK, while one key and one bill cover 295 routes across 20 modules. Its public discovery surface needs no key and exposes full request schemas, so an adapter can validate its contract before a provider move. The platform's idempotency convention has a 24h default deduplication window. Those conveniences don't remove the need to test recovery.
Compare control surfaces, then ownership costs
Vendor selection should begin with recovery and revocation semantics, not a login-widget demo. Auth0, Clerk, Firebase Authentication, and Supabase Auth are real managed options worth evaluating alongside a unified API or a self-hosted design. Prove the same scenario against each candidate rather than assuming similarly named methods have identical scope.
| Option | What to validate | Operational trade-off | Best fit |
|---|---|---|---|
| Auth0 | Session inventory, device termination, account-wide termination, and social recovery | A dedicated identity control plane, with its own tenancy model and integration contract | Teams wanting focused managed identity |
| Clerk | Per-session actions, notification hooks, and linked-identity recovery | Packaged application components, with policy coupled to Clerk's object model | Product teams valuing packaged sign-in UI |
| Firebase Authentication | Refresh-token revocation, validation timing, and provider recovery | Natural alignment with Firebase applications; incident tests must reflect its documented token model | Existing Firebase teams |
| Supabase Auth | Sign-out scopes, server enforcement, and linked-provider recovery | Managed and open-source choices; self-hosting transfers upgrades, capacity, and on-call ownership | Teams valuing Postgres alignment or deployment choice |
| Unified capability API | Stable list/revoke contract, readiness, audit evidence, and provider-change behavior | Fewer application contracts, but another dependency in the auth path | Platforms standardizing several backend capabilities |
| Direct self-host | Invalidation semantics, key rotation, durability, and emergency operations | Maximum control plus permanent patching, scaling, and incident ownership | Requirements that justify dedicated auth operations |
This is a buy-versus-build decision, but license cost is rarely the limiting resource. Count the pager: key rotation, provider changes, abuse handling, audit retention, data repair, and the synchronized sign-in wave after a broad revocation all belong in the capacity plan. Managed-service lock-in is real; so is lock-in to an internal system that only two engineers understand.
Use one acceptance suite across candidates. Create several sessions for one test user through both Google and GitHub, remove one selected session, prove the others still work, then perform the compromise path and prove every old session fails. Finally, change the affected credential and prove a fresh sign-in can establish a new session. The comparison becomes evidence instead of a feature-checkbox exercise.
Verify the blast radius and rehearse rollback
Verification has three layers. First, confirm that the selected session, or the full set for the user, is no longer accepted. Second, test the journey from a clean client through the intended Google or GitHub path. Third, inspect the security event and notification so an operator can answer who initiated the action, what scope was selected, and which recovery instruction the user received.
Do it quickly.
For single-session revocation, rollback is a new sign-in on that device; don't restore the revoked session. For revoke-all, recovery also means clean reauthentication after the credential has been repaired. Restoring old session material would reverse the security property you just established. If recovery fails, repair the provider callback, account linkage, or session issuance path, then retry from a clean client.
A production drill should record four numbers: time until old-session rejection, clean-sign-in success rate, callback error rate split by provider, and support contacts per affected account cohort. These are measurements to collect, not vendor performance claims. Set thresholds from your own SLO and threat model.
The decision rule fits in the runbook margin: one known device means revoke one; suspected account compromise means repair the credential and revoke all. Everything else keeps that rule observable, recoverable, and difficult to trigger by accident.
References
- OWASP Authentication Cheat Sheet
- OWASP Session Management Cheat Sheet
- NIST Digital Identity Guidelines: Authentication and Authenticator Management
- Auth0 User Sessions documentation
- Clerk session management documentation
- Firebase Authentication session management
- Supabase Auth sign-out documentation
Top comments (0)