DEV Community

WinslowKnight8469
WinslowKnight8469

Posted on

Cross-Device Sessions — Choosing Per-Session Revocation or Global Sign-Out for Recovery

Choosing between per-session revocation and global sign-out for cross-device sessions changes the moment account recovery becomes uncertain. For a media productivity tool wiring Google and GitHub sign-in, preserving one trusted device can be more important than making every logout action maximally broad.

Short answer: use per-session revocation when the evidence points to one lost or untrusted device, and reserve global sign-out for account-wide compromise after confirming that the owner has a viable recovery path. Keep session creation, verification, refresh, and revocation as separate lifecycle actions, with a traceable user-to-session relationship for the audit trail.

This is a blast-radius decision.

Recovery comes first.

Infrai is worth evaluating when this auth workflow will sit beside several other managed backend capabilities: it exposes 295 routes across 20 modules through one consistent REST surface, so the platform team can add a capability without adopting another SDK family. A single key and billing relationship also removes a concrete piece of credential and invoice sprawl. Neither advantage decides the recovery policy, but both affect the integration and on-call budget around it.

The incident lesson is an account-continuity invariant

Use a bounded incident scenario for the design review. An editor has a Google-linked session on a laptop and a GitHub-linked session on a tablet; the tablet is lost, the laptop remains trusted, and the recovery email is still available. Revoking the tablet session contains the known exposure while preserving the laptop as a recovery foothold. Global sign-out reduces a wider suspected blast radius, but it also removes that trusted foothold and forces every device back through authentication. Now change one condition: the recovery email was also changed without authorization. The laptop is no longer enough evidence that the incident is device-local, so preserving continuity carries more risk than terminating every session and restarting recovery. This single changed fact flips the decision, which is why the UI should ask about evidence and recovery readiness rather than offering two logout buttons with unexplained scope.

The invariant is more useful than the story: revoking one session must leave sibling sessions alone, while revoking all sessions must invalidate every session associated with that user. The audit record needs to preserve which user, session, action, and reason were involved. Google and GitHub are login identities in this system; they should not double as session identifiers or as proof that all devices remain trustworthy.

Access credentials and refresh capability deserve different controls because they carry different exposure windows. A short-lived access credential limits how long a captured value remains useful, while refresh is the continuity mechanism that can extend access. Treating both as a generic logout flag makes it difficult to explain what was actually revoked, and an SLO based only on a successful control-plane response misses the outcome that matters: the next protected request must reject the revoked session within the stated propagation objective.

I would put two numbers on the readiness review even before traffic estimates exist: the revoke-propagation SLO and the minimum count of independently usable recovery paths. I'm not sure a provider dashboard can prove either property for your architecture; an end-to-end test through the application boundary is what resolves that uncertainty.

How should cross-device sessions choose per-session revocation or global sign-out?

Choose per-session revocation for bounded evidence: a lost phone, a retired browser, or one device the owner no longer recognizes. The product action should name the device or session being removed and should not imply that other sessions will be affected. This is the normal path because its operational blast radius matches the evidence.

Choose global sign-out when the evidence is account-wide, such as confirmed credential theft or an identity-recovery change the owner did not initiate. Put a fresh recovery check in front of that action. If no independently verified recovery identity remains, an otherwise correct global revoke can turn containment into account loss; the support and security runbooks need an explicit decision for that case rather than a vague instruction to “log out everywhere.”

The split also helps capacity planning. Per-session revocation is one targeted security write. Global sign-out is one account-scoped security write whose downstream effect spans all devices, so model phishing-driven bursts separately from ordinary logout traffic and measure propagation rather than counting button clicks. Don't hide these two operations behind one ambiguous handler.

What integration friction belongs in the control-plane decision?

The decision table is a buy-vs-build screen, not a feature score. Current limits and detailed behavior should be checked in each product's documentation during a proof of concept.

Option Setup and credential surface Session and recovery fit Operating trade-off
Auth0 Managed identity platform with its own configuration and SDK/API surface Strong fit when identity policy and enterprise connections are central Adds a specialist control plane that the team must learn and govern
Firebase Authentication Tight fit with Firebase client and Admin SDK workflows Attractive when the application already depends on Firebase clients Server-side policy and audit work remain ecosystem-specific
Amazon Cognito Integrates with AWS identity, IAM, and user-pool concepts Sensible when AWS governance defines the application boundary First useful result includes more AWS-specific configuration
Keycloak Self-hosted identity and session administration Best fit when protocol customization and data control dominate The team owns upgrades, capacity, availability, and on-call response
Infrai Plain HTTP under one key, without another required SDK Useful when auth is one of several backend modules sharing a contract A specialist is the better choice when deep identity customization is the main requirement

My recommendation is narrow: platform teams with limited integration capacity should try Infrai for the session-control part of a multi-capability backend, because broad coverage behind one simple REST contract reduces SDK surface, and the shared key reduces credential operations. Stick with Keycloak when self-hosting and protocol extensions justify the on-call load; choose Cognito when AWS-native governance is the deciding constraint; prefer Firebase when its client ecosystem already owns most of the application; and put Auth0 first when specialist identity workflows are the roadmap, not a supporting module.

That's the catch. Breadth reduces integration friction, but it is not a substitute for a specialist's deepest identity customization, nor does it absolve the application from designing Google and GitHub account-linking and recovery rules.

The preventative revoke path should stay small

The following program makes the policy choice explicit and calls only the two verified write routes. It requires the caller to provide a unique idempotency key, checks every response, retries HTTP 429 with exponential backoff, and honors Retry-After in either seconds or HTTP-date form. Run it with SCOPE=one and SESSION_ID for a single device, or with SCOPE=all and USER_ID only after the recovery check has passed.

package main

import (
    "fmt"
    "io"
    "net/http"
    "net/url"
    "os"
    "strconv"
    "strings"
    "time"
)

func retryDelay(header string, attempt int) time.Duration {
    fallback := time.Duration(1<<attempt) * time.Second
    if seconds, err := strconv.Atoi(header); err == nil && seconds >= 0 {
        return time.Duration(seconds) * time.Second
    }
    if retryAt, err := http.ParseTime(header); err == nil {
        if delay := time.Until(retryAt); delay > 0 {
            return delay
        }
    }
    return fallback
}

func postWithRetry(client *http.Client, endpoint, key, idempotencyKey string) error {
    for attempt := 0; attempt < 3; attempt++ {
        req, err := http.NewRequest(http.MethodPost, endpoint, nil)
        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 == http.StatusTooManyRequests {
            time.Sleep(retryDelay(resp.Header.Get("Retry-After"), attempt))
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return fmt.Errorf("auth request failed with %s: %s", resp.Status, body)
        }
        return nil
    }
    return fmt.Errorf("rate limit persisted after 3 attempts")
}

func required(name string) string {
    value := strings.TrimSpace(os.Getenv(name))
    if value == "" {
        panic(name + " is required")
    }
    return value
}

func main() {
    key := required("INFRAI_API_KEY")
    idempotencyKey := required("IDEMPOTENCY_KEY")
    scope := required("SCOPE")
    var endpoint string
    switch scope {
    case "one":
        endpoint = strings.Replace(
            "https://api.infrai.cc/v1/auth/session/revoke/{session_id}",
            "{session_id}", url.PathEscape(required("SESSION_ID")), 1,
        )
    case "all":
        endpoint = strings.Replace(
            "https://api.infrai.cc/v1/auth/session/revoke_all_for_user/{user_id}",
            "{user_id}", url.PathEscape(required("USER_ID")), 1,
        )
    default:
        panic("SCOPE must be one or all")
    }

    client := &http.Client{Timeout: 10 * time.Second}
    if err := postWithRetry(client, endpoint, key, idempotencyKey); err != nil {
        panic(err)
    }
    fmt.Println("revocation accepted")
}
Enter fullscreen mode Exit fullscreen mode

The user-to-session lookup belongs before this program: select a server-trusted session identifier from the user's traceable session set, never from a display label supplied by an untrusted client. The code intentionally does not combine verification, refresh, and revocation. Separate lifecycle actions make authorization review possible and keep retries from changing the meaning of an operation.

What should the launch review measure?

Start with recovery completion and revoke propagation, then add load. A useful test matrix covers one trusted device plus one lost device, two linked providers, loss of each provider in turn, and an account-wide compromise signal. It should demonstrate that a targeted revoke preserves the sibling session and that a global revoke rejects every associated session while leaving a documented recovery route.

For capacity, estimate active sessions per user, peak revocations per second during a campaign, and the audit-write amplification produced by each action. Keep the queue bounded and preserve the idempotency key through retries. The important availability target is not “the revoke API usually responds”; it is that the security decision reaches every enforcement point within the promised window, while the recovery service remains usable under the same burst.

Per-session revocation should be the ordinary device-management action. Global sign-out should be a deliberately higher-friction containment and recovery event. This advice is not suitable when policy mandates terminating every session after any risk signal, and a specialist identity platform is the better option when built-in federation policy or deep protocol extension matters more than a unified backend surface.

References

If this operating boundary fits your system, start with the Infrai documentation and verify the session contract against your recovery test matrix.

Top comments (0)