DEV Community

FletcherVance3712
FletcherVance3712

Posted on

5 Steps for a Global Logout Workflow — Enumerate Sessions, Revoke All, Verify Results

Short answer: make global logout a transaction over every session record, revoke the server-side authority first, then verify from a fresh client that old credentials cannot create an authenticated request.

In a logistics system, this matters most when a stolen dispatcher cookie can mint labels, alter delivery addresses, or turn a real device fingerprint into a trusted bot. “Log out everywhere” is therefore an abuse-resistance workflow, not a button that deletes one browser cookie. The design below treats revocation as a ledger event: idempotent, auditable, and explicit about what the system can prove.

1. Define the authority you are revoking

Start with a session inventory whose rows have a stable session ID, subject ID, credential family, creation time, last-seen time, device-fingerprint reference, and a revocation state. Keep refresh tokens, browser sessions, mobile sessions, and service-issued login grants distinguishable; they often have different expiry and verification paths. A logout request should identify the subject and the reason, but it should not trust a device-supplied list of sessions.

The first failure mode is scope ambiguity. Revoking a cookie while leaving its refresh-token family valid gives an attacker a way back in. It fails. Revoking only the latest token leaves older mobile sessions alive, which is why I don't treat a browser response as evidence that the account is clean. For a global action, the authoritative set is the server-side inventory for that subject at a defined cutoff, plus a subject-level invalidation marker that catches tokens created during the operation.

Audit the decision. Record who initiated it, request correlation ID, inventory count, cutoff timestamp, and the resulting event ID. Do not put raw tokens or full fingerprints in that record; a keyed digest or internal reference is enough for reconciliation.

2. How should you enumerate sessions, revoke all, and verify the result?

Use a repeatable sequence with a clear snapshot boundary:

  1. Authenticate the actor with a recent step-up where policy requires it, and create an idempotency key for the logout command.
  2. Read all active session IDs for the subject under a transaction or consistent snapshot. Paginate deterministically by (created_at, session_id) so a large account cannot produce a partial, unstable list.
  3. Write a subject-level logout_epoch (or equivalent version) and mark every snapshot session revoked. The token validator must reject a token whose issue time or session version predates that marker.
  4. Propagate the revocation event to caches, token introspection, websocket gateways, and background job consumers. A queue retry must apply the same event safely; duplicate delivery must not resurrect a session.
  5. Verify with a new request context: enumerate again, introspect representative credential types, and attempt an authenticated operation with each previously valid credential. Expect denial, not merely a successful delete response.

The cutoff closes a race: a login completed while page two was being read still fails the subject-level check. Your retention policy may later archive the rows, but deletion is not proof of revocation.

Here is a deliberately small Go shape for the command boundary. The storage implementation can be SQL, a key-value store, or an identity provider adapter; the contract is what matters.

package auth

import "context"

type Session struct {
    ID      string
    Version uint64
}

type Store interface {
    Snapshot(ctx context.Context, subject string) ([]Session, uint64, error)
    RevokeSubject(ctx context.Context, subject string, epoch uint64, key string) error
    RevokeSessions(ctx context.Context, ids []string, key string) error
}

func GlobalLogout(ctx context.Context, s Store, subject, key string) error {
    rows, epoch, err := s.Snapshot(ctx, subject)
    if err != nil {
        return err
    }
    if err := s.RevokeSubject(ctx, subject, epoch, key); err != nil {
        return err
    }
    ids := make([]string, 0, len(rows))
    for _, row := range rows {
        ids = append(ids, row.ID)
    }
    return s.RevokeSessions(ctx, ids, key)
}
Enter fullscreen mode Exit fullscreen mode

The function is safe to retry only if both writes are idempotent on key; otherwise a client timeout can leave operators unsure whether the command ran. That uncertainty is operational debt.

3. Make verification an observable security check

A green HTTP response is weak evidence. Verification should compare the pre-action inventory with a post-action query, then exercise the same validation path used by production traffic. Check browser cookies, refresh tokens, mobile credentials, and any gateway session separately because a revoked record in one subsystem says nothing about another.

I keep a compact result record: requested, snapshot_count, revoked_count, already_revoked, post_active_count, and denied_replay_count. Alert when post_active_count is nonzero after the propagation window, or when replay denial is lower than the number of credential classes tested. Your mileage may vary on the window length; measure cache and queue latency in your own deployment instead of borrowing a universal timeout.

Three words: prove the negative.

For a logistics operator, also test a device-fingerprint change. A fingerprint should be a risk signal, not a bypass around revocation. A new device must still fail subject-level authorization until the user signs in again and receives a new session version.

4. Handle failure, retries, and compliance boundaries

Treat the operation like a small distributed transaction with compensating observation rather than pretending the network is exactly-once. The event log is the source for reconciliation; consumers store the last applied event ID and ignore older or duplicate events. If a consumer is offline, keep the event pending and expose that state to operators. Do not claim completion until the required validators have observed the marker.

Limit logs to what an incident responder needs. OWASP recommends server-side session invalidation and warns against relying on client-side termination alone; privacy rules may also constrain retention of device identifiers and IP addresses. Consult your data-protection officer about purpose, retention, and access controls before exporting session inventories to analytics.

The catch is that global logout cannot recall an already accepted shipment update or revoke a credential cached by an external system you do not control. Use short token lifetimes, sender-constrained tokens where available, and downstream authorization checks for high-impact actions. Stick with a local-only logout when the requirement is merely to close one browser and preserve other trusted devices.

5. Roll out with a reconciliation loop

Ship the workflow behind an audit-only mode that enumerates and records the intended set without changing authorization. Compare counts across the session store, token introspection layer, cache, and gateway. Then enable revocation for internal accounts, inject retries and out-of-order events in staging, and sample real production verifications before making the control user-visible.

Keep a runbook that names the event ID, subject, cutoff, consumers, and final verification evidence. A support agent should be able to answer “which sessions were revoked?” without asking an engineer to search raw logs. That is the practical test of auditability.

Global logout is complete when every authorization path enforces the new subject version and the evidence survives a replay check. Enumeration, revocation, and verification are one workflow; splitting them across unrelated buttons creates the gap an attacker will use.

References

Top comments (0)