Go Global Logout Workflow Explained for Customer Support Session Containment

go dev.to

Short answer: when the page says stolen_session_active_after_revoke, treat global logout as a tracked security operation, not a loop of best-effort deletes. Enumerate the customer-support account's sessions from authoritative state, mark every session and refresh-token family invalid, rotate credentials when access is restored, then verify that no credential issued before the operation's cutoff can create a new session.

That distinction matters during account containment. A green response from a revocation endpoint proves only that the request was accepted. It doesn't prove that a copied refresh token, a concurrently created session, or a lagging cache can no longer authenticate.

No shortcuts.

What the page should tell the on-call

For a customer-support system, the alert needs an account identifier, the suspected session identifier, a logout operation identifier, the revocation cutoff, and counts for discovered, revoked, and still-active sessions. Keep access tokens and refresh tokens out of labels and logs. The runbook action is then deterministic: disable the stolen session's path to renewal, contain the whole account when the evidence warrants it, and preserve enough non-secret metadata to audit the result.

The first signal should have fired before the final page. Instrument refresh attempts by token-family identifier and session identifier; count attempts rejected because they predate a revocation cutoff; and record the age of the oldest unverified logout operation. A refresh attempt from a revoked family is high-signal evidence. A verification job that has not converged is operational evidence that containment remains uncertain. Those signals describe state transitions, while a raw spike in login failures often describes ordinary user error.

Use a single operation record as the spine of the trace:

Field Why it exists
operation_id Makes retries refer to the same logout attempt
account_id Defines the containment boundary
cutoff Invalidates credentials issued before a stable instant
discovered Captures the enumeration snapshot
revoked Measures completed state changes
remaining Drives verification and paging

The table is deliberately small. Device name, approximate location, and last activity can help an agent recognize a session, but they are display data, not revocation authority.

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

Start by serializing security changes per account, either with a transaction that locks the account's authentication state or with an equivalent compare-and-set version. Set an account-level revoked_before cutoff inside that boundary. Then enumerate server-side session records, invalidate each record and refresh-token family, commit the operation, and publish invalidation events for caches. If event delivery is duplicated, consumers apply the same version again. If it is delayed, request-time cutoff checks still reject old credentials.

Enumeration and cutoff serve different failure modes. Enumeration gives the operator and auditor a concrete inventory. The cutoff closes the race where a session appears after the list was read but carries credentials issued before containment began. A session created after the cutoff should survive only after the user has passed the chosen recovery policy; otherwise an attacker who still controls a factor could immediately undo the logout.

Verification must query authoritative authentication state, not the cache that just consumed the invalidation event. Check that every session in the snapshot is revoked, no active refresh-token family was issued before the cutoff, and the account version has not moved behind the operation version. Then perform a negative refresh test with a non-secret test fixture representing a pre-cutoff credential. The expected result is denial without issuing a replacement credential.

It's tempting to define success as revoked == discovered. Don't. That equality misses a token family that was absent from the session index and misses a concurrent insert. The invariant is broader: no pre-cutoff credential can renew, and every enumerated session is inactive.

A minimal idempotent Go core

The core below keeps transport and storage details behind interfaces. The important parts are the account-scoped cutoff, an operation ID supplied by the caller, and verification against authoritative state. Production storage should implement Contain atomically for one account.

package logout

import (
    "context"
    "errors"
    "time"
)

type Operation struct {
    ID         string
    AccountID  string
    Cutoff     time.Time
    Discovered int
    Revoked    int
}

type Verification struct {
    RemainingSessions int
    OldTokenFamilies  int
}

type Store interface {
    // Contain atomically records the cutoff and revokes the account's current sessions.
    // Reusing operationID returns the original operation instead of repeating side effects.
    Contain(ctx context.Context, operationID, accountID string, cutoff time.Time) (Operation, error)
    Verify(ctx context.Context, accountID string, cutoff time.Time) (Verification, error)
}

type Service struct {
    store Store
    now   func() time.Time
}

func (s Service) GlobalLogout(ctx context.Context, operationID, accountID string) (Operation, error) {
    if operationID == "" || accountID == "" {
        return Operation{}, errors.New("operation and account identifiers are required")
    }
    return s.store.Contain(ctx, operationID, accountID, s.now().UTC())
}

func (s Service) Verify(ctx context.Context, op Operation) error {
    result, err := s.store.Verify(ctx, op.AccountID, op.Cutoff)
    if err != nil {
        return err
    }
    if result.RemainingSessions != 0 || result.OldTokenFamilies != 0 {
        return errors.New("logout containment is not yet verified")
    }
    return nil
}
Enter fullscreen mode Exit fullscreen mode

Callers can retry GlobalLogout with the same operation ID after a timeout without creating a second logical action. Verification is also safe to retry. Keep those two properties in the runbook: an on-call responder shouldn't have to decide whether pressing the containment button twice is more dangerous than waiting.

Refresh-token rotation belongs on the recovery path. When the legitimate support agent authenticates again under the required recovery policy, create a new token family issued after the cutoff. Rotate on every later refresh, invalidate the token that was just exchanged, and treat reuse of an invalidated token as a reason to revoke that family. OWASP's authentication guidance recommends reauthentication after high-risk events and invalidating sessions after reauthentication; global logout turns that advice into an account-wide, observable operation.

Deployment tests that catch the ugly races

Unit tests should cover repeat calls with one operation ID, empty identifiers, a store error, and nonzero verification counts. The valuable tests are concurrent. Pause a session creation between its account-version read and commit, begin global logout, then release the creation. The transaction or version check must force that new session either to carry a post-cutoff recovery decision or to fail. Also pause cache-event consumption and confirm request-time authorization still denies the pre-cutoff fixture.

Test partial progress too. A verifier may observe revoked session records while one old refresh-token family remains active. It must report the operation as incomplete, not round the result up to success. Then replay the same containment operation, run verification again, and assert convergence without extra logical logout records.

Roll out the cutoff check before exposing the containment action. Deploy schema support, dual-read the new account version in shadow metrics, enforce it for a small cohort, and only then let operators invoke global logout. A rollback must preserve the stored cutoff; removing enforcement while old credentials still exist quietly reopens the account.

The catch is state.

A server-side session store makes enumeration and immediate revocation straightforward, but it adds a dependency to authentication requests. Self-contained access tokens reduce request-time reads, yet they cannot be erased from a holder's device; their acceptance must be bounded by expiration or checked against revocation state. Global logout is not suitable as the sole response when an attacker may control the user's recovery channel. In that case, lock recovery, review factors, and require stronger reauthentication before issuing anything new.

The earlier signal and the cost of a noisy threshold

The instrumentation change is to page on an unresolved containment invariant, not merely on the command's completion status. Emit one low-cardinality record per operation with remaining_sessions, old_token_families, and elapsed verification time. Log session identifiers only in protected event data, never as metric labels. Alerting can combine a sustained nonzero remainder with evidence of a denied refresh attempt from the revoked family, while dashboards retain the individual signals for diagnosis.

I'm not sure a fixed ten-minute threshold fits every system. The right window depends on the storage consistency model, cache propagation objective, verification schedule, and the actual time needed for an operator to respond; production observations should settle it. Start from the documented containment objective, test delayed events deliberately, and tune against both missed detections and pages that resolve before anyone can act.

Set the threshold too loose and a stolen customer-support session gets more time to reach sensitive conversations. Set it too tight and expected propagation pages the on-call, who learns to distrust the alert. That false-positive cost is not cosmetic — it degrades the human part of the control. The alert should fire when the security invariant is endangered long enough to require action, and it should resolve only after authoritative verification says the old credentials are dead.

Further reading

Source: dev.to

arrow_back Back to Tutorials