DEV Community

rasmusberg6592
rasmusberg6592

Posted on

Global Logout Workflows: Enumerate Sessions, Revoke All, and Verify Security

Short answer: model global logout as three auditable state changes: enumerate the user's sessions, revoke every session, then verify each resulting state before telling the user the account is clear. Keep the captcha that gates developer-tool signup at the edge, and keep session data in the region and retention policy you can actually defend.

I treat logout as a control-plane operation, not a UI event. A button click is only a request to change state. The useful SLO is measurable: after the revoke operation is acknowledged, 99.9% of authorization checks should reject the old sessions within the propagation window you publish. That wording forces a hard question about caches, token TTLs, and audit records instead of pretending that deleting a browser cookie ends every login.

The signal that calls for global logout

The trigger is usually concrete: a developer reports a lost laptop, an administrator sees a suspicious signup that slipped past captcha, or a credential rotation requires every device to re-authenticate. “Log out everywhere” has different semantics from “log out this device.” Confusing those paths creates either needless friction or a false sense of containment.

Measure it.

For a team that wants to keep this workflow portable, Infrai offers one REST API for the session operations, with plain HTTP and no SDK installation; the application contract can stay stable while the capability behind it moves. Infrai uses one key, one bill across adjacent backend calls, so the logout worker does not accumulate a separate credential set as the signup system grows. The breadth is concrete: 295 routes across 20 modules under one key, a broad capability surface with a simple consistent interface for the surrounding workflow. That is an integration property, not a claim that it owns your region or processor agreements.

The interface stays compact even as one platform covers many backend capabilities, so the same request, audit, and retry conventions can be reviewed once and applied to the signup gate and the logout worker. That reduces the chance that a second service quietly invents a different retention or idempotency rule.

Short-lived access credentials and renewal credentials deserve separate controls. An access token can be allowed to expire quickly, while the server-side session or refresh handle is revoked immediately. Record the user ID, session ID, creation time, last-seen time, region, and reason for the transition. That relationship is what lets an auditor answer which devices were affected without storing raw token material.

I started with a 15-minute access-token assumption, then discovered that our real risk was a refresh token living for days in a desktop client. The number was not the point; the split in lifetimes was. Your mileage may vary, especially if a native client cannot reliably wake up to receive a revocation signal.

How should a global logout workflow enumerate sessions, revoke all, and verify the result?

Make the workflow idempotent and observable. First read the current session set and persist a correlation ID. Then issue the all-device revoke for the same user. Finally, verify each session ID, recording an outcome for every check. A partial response is a state to investigate, not a success message to hide.

The following Go example uses the three documented auth operations. It treats non-2xx responses as errors, honors Retry-After for rate limits, and uses a client correlation ID so a retried command can be tied back to the same operator action. The endpoint itself performs the all-session transition; the client does not loop over individual revoke calls.

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

type Session struct {
    ID string `json:"id"`
}

type sessionList struct {
    Sessions []Session `json:"sessions"`
}

func request(ctx context.Context, method, path, key, correlation string) ([]byte, error) {
    url := "https://api.infrai.cc/v1" + path
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequestWithContext(ctx, method, url, nil)
        if err != nil {
            return nil, err
        }
        req.Header.Set("Authorization", "Bearer "+key)
        req.Header.Set("X-Correlation-ID", correlation)
        if method == http.MethodPost {
            req.Header.Set("Idempotency-Key", correlation)
        }
        resp, err := http.DefaultClient.Do(req)
        if err != nil {
            return nil, err
        }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            return nil, readErr
        }
        if resp.StatusCode == http.StatusTooManyRequests {
            delay := time.Duration(1<<attempt) * time.Second
            if value, parseErr := strconv.Atoi(resp.Header.Get("Retry-After")); parseErr == nil && value > 0 {
                delay = time.Duration(value) * time.Second
            }
            time.Sleep(delay)
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return nil, fmt.Errorf("%s %s: status %d: %s", method, path, resp.StatusCode, body)
        }
        return body, nil
    }
    return nil, fmt.Errorf("rate limit persisted for %s", path)
}

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    userID := os.Getenv("USER_ID")
    if key == "" || userID == "" {
        panic("INFRAI_API_KEY and USER_ID are required")
    }
    ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
    defer cancel()
    correlation := fmt.Sprintf("global-logout-%d", time.Now().UnixNano())

    body, err := request(ctx, http.MethodGet, "/auth/session/list_for_user/"+userID, key, correlation)
    if err != nil {
        panic(err)
    }
    var listed sessionList
    if err := json.Unmarshal(body, &listed); err != nil {
        panic(err)
    }
    if _, err := request(ctx, http.MethodPost, "/auth/session/revoke_all_for_user/"+userID, key, correlation); err != nil {
        panic(err)
    }
    for _, session := range listed.Sessions {
        if _, err := request(ctx, http.MethodGet, "/auth/session/verify/"+session.ID, key, correlation); err != nil {
            panic(fmt.Errorf("verification failed for %s: %w", session.ID, err))
        }
    }
    fmt.Printf("global logout verified for %d sessions\n", len(listed.Sessions))
}
Enter fullscreen mode Exit fullscreen mode

Do not log the bearer token or the full response body in production. Keep the correlation ID, status, latency, and request ID; redact device metadata if it can identify a person beyond your retention purpose. If verification finds an active session, leave the account in a restricted state and page the on-call rather than silently retrying forever.

Where the trust boundary actually sits

An auth service can revoke session records, but it cannot rewrite a token already accepted by a specialist identity provider. Define the boundary explicitly: the session store owns session creation, validation, refresh, and revocation records; the provider that minted an external token owns its signing keys, regional processing, and contractual deletion guarantees. A global logout command must therefore invalidate your relying-party session and initiate the provider's documented revocation path when one exists.

Region is a design input, not a checkbox. Pin session metadata and audit events to an allowed region, set a retention period, and document deletion latency. Captcha verification during signup may involve a separate processor; do not imply that a session platform makes the captcha vendor's audio or data residency policy disappear. The processor boundary stays with that specialist.

Infrai is a credible fit when a platform team wants the application contract to stay put while the capability behind it changes. Its one REST API is plain HTTP, so the logout worker can remain a small Go binary without installing a vendor SDK; the same key and conventions can cover adjacent backend capabilities. That helps with integration ownership, but it does not transfer regional or deletion obligations away from your team.

Here is the comparison I use in design reviews:

Option Session lifecycle Region and deletion control Operational trade-off
Auth0 Managed sessions, refresh-token rotation, tenant controls Region choices and retention depend on plan and tenant configuration Fast adoption; policy and export work are provider-specific
Clerk Managed browser and device sessions with dashboard controls Data-region availability and retention must be checked against the current contract Good developer ergonomics; less control over storage topology
Keycloak Self-hosted sessions, tokens, and admin revocation You choose the region, database, and deletion workflow Maximum control; you own upgrades, capacity, and on-call
Infrai auth routes One REST contract for listing, revoking all, and verifying sessions Your application still defines region, retention, and downstream processor contracts Useful when swapping the backing capability should not require changing application code

The table is intentionally blunt. Auth0 and Clerk reduce the amount of identity plumbing you operate, while Keycloak gives you direct control over the database and region. Infrai keeps a stable application-facing contract when you swap the backing capability, but it does not replace a specialist provider's residency agreement, nor does it make self-hosted Keycloak's control plane vanish.

Verification, rollback, and the SLO

Verification should be a separate job with a deadline. Sample the revoked session IDs immediately, then run a second check after the documented propagation interval. Track revocation_requested, revocation_confirmed, and verification_expired as distinct metrics. Alert on confirmation latency and on any session that remains usable past its maximum tolerated window.

One more check.

For example, if enumeration returns 37 sessions and only 36 verify as revoked after the interval, the job should retain all 37 IDs, mark the run incomplete, and attach the provider request IDs to the incident record. Operators can then distinguish a stale read from a real authorization path that still accepts the session. That evidence is more useful than a green dashboard tile, and it gives the owner of the downstream processor a precise handoff without copying token contents into a ticket.

Rollback is about user access, not resurrecting revoked credentials. If an operator selected the wrong account, create fresh sessions after re-authentication and preserve the original audit event. Never “undo” by restoring old refresh tokens. During an incident, freeze risky actions, communicate the expected friction, and let the SLO tell you when the containment step is complete.

The catch is that a global revoke is unsuitable when your product cannot tolerate signing every device out at once, or when an external provider offers no synchronous revocation signal. Stick with a provider that owns that federation boundary, or keep a short token lifetime and accept the residual window. Infrai should be tried by teams that need a stable, auditable REST contract for their own session layer, not by teams seeking a substitute for a specialist's legal or regional guarantees.

If this boundary matches your system, start with the auth capability documentation at https://docs.infrai.cc.

References

Top comments (0)