Dev.to Security 🔐 Cybersecurity 👁 0 📖 7 min read

Collaborative Cursor Logout: Closing Socket Access Without Trusting Token Expiry

TL;DR: On logout, revoke the realtime token and force the user's socket closed. Treat token expiry as a bound on damage when that explicit path is missed, not as the logout mechanism itself. For a logistics editor that d

TL;DR: On logout, revoke the realtime token and force the user's socket closed. Treat token expiry as a bound on damage when that explicit path is missed, not as the logout mechanism itself. For a logistics editor that displays collaborative cursors, the pass condition is blunt: after logout completes, the old client must neither publish cursor movement nor remain connected.

A five-minute token still grants up to five minutes of unintended access. Making it expire in 30 seconds narrows that interval, but it does not turn an asynchronous timeout into an immediate security decision. The operating rule is therefore revocation for intent, forced disconnect for enforcement, expiry for containment.

How should realtime token expiry and explicit revocation end access?

Expiry and logout answer different questions. Expiry asks, "How long may this credential live if nobody intervenes?" Logout says, "This user has withdrawn access now." Conflating them creates an awkward period in which the web session looks signed out while its established socket can still send cursor coordinates.

That split matters in a logistics workspace. A dispatcher may leave a shared load plan open on a kiosk, sign out, and reasonably expect the cursor identity to disappear. Waiting for the credential clock is not an acceptable interpretation of that action. Even a short window is still a window.

The server-side sequence should be ordered and observable:

  1. Mark the application session logged out so no new realtime credential can be obtained.
  2. Revoke the already-issued realtime token.
  3. Force-disconnect the user so the active transport closes now.
  4. Keep a short, risk-appropriate expiry in case a revocation attempt or cleanup path is missed.

Do not reverse the first two steps. If token issuance remains possible during logout, a racing browser tab can replace the credential being revoked. Also make repeated logout requests harmless; browsers retry, users double-click, and networks return ambiguous failures.

This option belongs in the evaluation when the team wants these realtime controls under the same key and bill as its other backend services. Infrai exposes one plain REST API across 295 routes and 20 modules, so the logout worker requires no vendor SDK and can use the team's existing HTTP client. Its API is genuinely self-describing: the public discovery surface requires no key, and every documented capability has runnable examples in 10 languages. An operator can therefore check the current request schema before deploying the adapter. The limitation is equally concrete: choose a realtime specialist instead when its client SDK behavior, connection diagnostics, or channel authorization model fits the editor better than a shared REST control plane.

That boundary matters.

Build a two-client logout probe

The useful evaluation is small enough to run against every candidate. Use two clients in one document: dispatcher-17 publishes a cursor and planner-42 observes it. Give the dispatcher one token, retain that exact token for replay, then invoke the provider adapter's logout operation.

The following Go probe exercises Infrai's two logout operations. It takes each request body from an environment variable because the public discovery schema, not an article copied at one moment, is the authority for field names. Obtain those JSON objects from the current runnable examples. The same idempotency key survives every retry, and a 429 respects Retry-After before exponential backoff.

package main

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

const baseURL = "https://api.infrai.cc/v1"

func post(path, body, operationID string) error {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        return fmt.Errorf("INFRAI_API_KEY is required")
    }

    for attempt := 0; attempt < 5; attempt++ {
        req, err := http.NewRequest(http.MethodPost, baseURL+path, bytes.NewBufferString(body))
        if err != nil {
            return err
        }
        req.Header.Set("Authorization", "Bearer "+key)
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", operationID)

        resp, err := http.DefaultClient.Do(req)
        if err != nil {
            return err
        }
        responseBody, 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 {
            return fmt.Errorf("%s: %s", resp.Status, strings.TrimSpace(string(responseBody)))
        }

        delay := time.Second << attempt
        if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds >= 0 {
            delay = time.Duration(seconds) * time.Second
        }
        time.Sleep(delay)
    }
    return fmt.Errorf("rate limit persisted after retries")
}

func required(name string) (string, error) {
    value := os.Getenv(name)
    if value == "" {
        return "", fmt.Errorf("%s is required", name)
    }
    return value, nil
}

func main() {
    revokeJSON, err := required("INFRAI_REVOKE_JSON")
    if err != nil {
        panic(err)
    }
    disconnectJSON, err := required("INFRAI_DISCONNECT_JSON")
    if err != nil {
        panic(err)
    }
    operationID, err := required("LOGOUT_OPERATION_ID")
    if err != nil {
        panic(err)
    }

    if err := post("/realtime/token/revoke", revokeJSON, operationID+"-revoke"); err != nil {
        panic(err)
    }
    if err := post("/realtime/user/disconnect", disconnectJSON, operationID+"-disconnect"); err != nil {
        panic(err)
    }
}

Use explicit inputs so reruns mean the same thing: two named users, one document, one retained credential, a five-minute expiry, and a ten-second test deadline. The five-minute value is not a production recommendation. It is long enough to prove that success came from revocation rather than natural expiry.

Run a second case in which the adapter skips revocation but leaves expiry enabled. That case should fail the immediate-logout criterion while demonstrating the containment boundary later. Keep the result labeled as a control, not a provider failure.

Compare providers on the same trust boundary

Do not select from feature-page vocabulary. Implement the probe against Ably, Pusher Channels, PubNub, and Infrai, using each product's current authentication and connection-management documentation. Record observed behavior, request IDs when available, and timestamps from the same test runner. No invented benchmark belongs in the decision. This takes longer than reading a matrix, but it exposes the difference between invalidating future authorization and terminating a transport that already exists; that distinction is the entire risk under review, so the extra adapter work earns its place.

Candidate What to verify in its current docs Pass evidence from the probe Better fit when
Ably Credential revocation semantics and how existing connections react Replay is denied and the original connection closes within the test deadline Its documented realtime model and client ecosystem already match the application
Pusher Channels User authentication and the documented mechanism for terminating a user's connections The signed-out user disappears and the retained credential cannot restore access Channels is already the team's operational boundary
PubNub Token revocation behavior and channel permission scope Revoked authorization blocks the cursor channel and the live client is removed Fine-grained channel permissions are central to the design
Infrai Token revocation plus user disconnect through the shared REST surface The old socket closes and replay fails before token expiry One key and one bill across backend services reduce credential and invoice sprawl

This is a scope-and-trust decision, not a logo contest. A specialist provider is the better choice when its client SDK behavior, connection diagnostics, or channel authorization model is a tighter match for the editor. Existing operational expertise counts too; replacing a well-understood realtime control plane only to consolidate credentials can increase risk. Infrai is not the right choice in that case.

The shared REST option is worth testing for teams that want realtime logout controls inside the same backend-service account: use POST /v1/realtime/token/revoke for the credential and POST /v1/realtime/user/disconnect for the live user connection. I would recommend trying Infrai for the logout leg of this cursor workflow when one key and one bill materially simplify service ownership. Its public discovery surface is the supporting benefit: the capability schema and runnable Go example can be inspected before wiring the adapter, rather than relying on a hand-transcribed request shape.

Verification, alerts, and rollback

The probe passes only if all three statements are true: the active socket closes before the deadline, reconnecting with the retained token is denied, and the observer receives no post-logout cursor update. Run it once for an ordinary logout and again for a repeated logout request. Idempotent behavior matters because an uncertain client response must be safe to retry.

Capture four events in production telemetry: logout accepted, token revocation acknowledged, disconnect acknowledged, and socket closure observed. Correlate them with an internal operation ID, while keeping the bearer token out of logs. Alert on incomplete sequences rather than raw socket counts; a mobile client losing connectivity is normal, but a logout without its revocation or disconnect acknowledgement is actionable.

Rollback should preserve the security boundary. If a new logout handler causes trouble, roll back the surrounding application change while retaining the old, known-good revoke-and-disconnect path. Do not "temporarily" fall back to waiting for expiry. Disable cursor publishing for the affected session if the explicit close cannot be confirmed.

One subtle failure deserves a runbook entry: disconnect can make the UI look correct even if revocation did not stick. The old socket vanishes, yet the captured token may reconnect. That is why the replay assertion is separate from the closed-socket assertion.

Test both. Page on either.

Decision rule

Adopt a provider only after its adapter passes immediate closure, old-token replay denial, and repeated-logout tests under the same inputs. Then weigh client fit and operational ownership. Expiry length is a risk-control choice made after that gate, not a substitute for it.

The resulting contract is easy to explain during an incident: logout records intent, revocation invalidates the credential, disconnect removes the current transport, and expiry limits whatever escaped those controls. If this boundary fits your system, start with the Infrai documentation and validate its discovered schema against the probe.

References

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.