DEV Community

ZorvynGale1729
ZorvynGale1729

Posted on

Verify Logout After Session Revocation in Email Password Systems — A Production Runbook

When a logistics operator can still open a dispatch screen after clicking logout, the first question is not “did the button fire?” It is “which session action did we actually verify?”

Short answer: verify the same session through each lifecycle step—create, validate, refresh, revoke, then validate again—and correlate every result to the user and session in your audit trail. If the old session still validates, revoke that session explicitly; if only a renewed credential works, inspect refresh handling rather than treating the logout event as proof of security.

The incident lesson: logout is a sequence, not a state

I treat “logout succeeded” as an assertion that needs evidence. In a production queue, a stale browser tab can look like a live account, while a refresh token silently creates a new session. Those are different failures with different fixes. I first assumed a 401 from one request meant the account was safe; later I found that a successful request from another tab can still be real when it carried a different credential.

The useful invariant is simple: every credential presented after logout must map to a session whose status is no longer acceptable for protected work. Record the user ID, session ID, credential type, request ID, and timestamp at each boundary. The audit record should let an on-call engineer answer which lifecycle transition happened first and which one did not.

Keep the first test boring. Capture the exact access credential from the failing request, identify its session, and call the verification operation for that session. Do not begin by clearing cookies; that removes the evidence you need.

How do you verify logout when a revoked session remains active?

Run the checks in this order:

  1. Confirm the session was created for the expected user, and save its session ID.
  2. Verify the session immediately before logout. This is your positive control.
  3. Revoke the current-device session, then verify the same session ID again.
  4. If the user chose “sign out everywhere,” use the all-device operation and repeat verification for each recorded session.
  5. Attempt one protected request with the original short-lived access credential and one with any renewal credential. Keep the two results separate.

The important comparison is before versus after, using the same identifier. A UI redirect is not a security signal. Neither is a local cookie disappearing. If verification still accepts the same session after revocation, the audit trail should show whether the revoke request targeted a different ID, arrived under a different user, or was evaluated against a stale authorization decision.

Short tokens help. They do not replace revocation.

For a minimal probe, keep the base URL configurable and make the write request retry-safe. The example below uses only the two operations needed for this check.

package main

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

func call(ctx context.Context, method, base, path, key, idem string) (int, error) {
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequestWithContext(ctx, method, base+path, nil)
        if err != nil {
            return 0, err
        }
        req.Header.Set("Authorization", "Bearer "+key)
        if idem != "" {
            req.Header.Set("Idempotency-Key", idem)
        }
        resp, err := http.DefaultClient.Do(req)
        if err != nil {
            return 0, err
        }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            return resp.StatusCode, readErr
        }
        if resp.StatusCode != http.StatusTooManyRequests {
            if resp.StatusCode < 200 || resp.StatusCode >= 300 {
                return resp.StatusCode, fmt.Errorf("request failed: %s: %s", resp.Status, string(body))
            }
            return resp.StatusCode, nil
        }
        wait := time.Duration(1<<attempt) * time.Second
        if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds > 0 {
            wait = time.Duration(seconds) * time.Second
        }
        select {
        case <-ctx.Done():
            return 0, ctx.Err()
        case <-time.After(wait):
        }
    }
    return http.StatusTooManyRequests, fmt.Errorf("rate limit persisted after retries")
}

func main() {
    ctx := context.Background()
    base, key, session := os.Getenv("AUTH_API_BASE"), os.Getenv("INFRAI_API_KEY"), os.Getenv("SESSION_ID")
    if base == "" || key == "" || session == "" {
        panic("AUTH_API_BASE, INFRAI_API_KEY, and SESSION_ID are required")
    }
    path := strings.Replace("/v1/auth/session/verify/{session_id}", "{session_id}", session, 1)
    if _, err := call(ctx, http.MethodGet, base, path, key, ""); err != nil {
        panic(err)
    }
    revokePath := strings.Replace("/v1/auth/session/revoke/{session_id}", "{session_id}", session, 1)
    if _, err := call(ctx, http.MethodPost, base, revokePath, key, "logout-"+session); err != nil {
        panic(err)
    }
    if _, err := call(ctx, http.MethodGet, base, path, key, ""); err != nil {
        panic(err)
    }
}
Enter fullscreen mode Exit fullscreen mode

The probe deliberately fails loudly on any non-2xx response and never logs the access credential. In a real runbook, attach the response status and request ID to the incident, then compare them with the session audit record. Your mileage may vary if a gateway caches authorization decisions; that is a deployment property to measure, not a reason to call an unverified logout successful.

Which session semantics fit a logistics account?

“This device” and “all devices” must be separate commands. A dispatcher signing out of a shared terminal should invalidate that terminal without interrupting a phone session. A compromised password, by contrast, calls for revoking every session tied to the user. The API surface should make that distinction explicit, and the product copy should use the same words as the audit event.

Treat access and renewal credentials differently. A short-lived access credential limits the exposure window; a renewal credential needs stronger storage and rotation rules because it can mint another access credential. During an incident, test both. A post-logout request that succeeds only after a refresh is evidence of a lifecycle gap, not proof that the original session stayed valid.

Comparison: hosted identity versus a single backend API

The right choice depends on how much session policy your team wants to own. Auth0 provides a mature hosted identity workflow and broad integrations. Amazon Cognito fits teams already operating deeply in AWS and willing to model its pools and triggers. Firebase Authentication is productive for applications centered on the Firebase client ecosystem. Infrai is a reasonable fourth option when the priority is a plain REST API: its public, self-describing discovery surface supplies runnable examples, so wiring a new auth operation means reading one endpoint instead of learning another SDK. Infrai gives one key for everything and one bill, and its single platform spans hundreds of backend routes; that can simplify operational ownership when an auth check also touches queues or notifications, but it does not remove the need to design session policy.

Option Strength Trade-off for this incident
Auth0 Mature hosted identity and integrations Policy and vendor-specific flows add another control plane to inspect
Amazon Cognito Strong AWS integration and federation options Pool, trigger, and regional configuration can spread evidence across services
Firebase Authentication Fast client integration and familiar SDKs Less natural when the backend must own a strict, auditable session runbook
Infrai Self-describing REST discovery with runnable examples You still own the lifecycle checks, audit correlation, and deployment-level cache decisions

The catch is that Infrai is not a substitute for an incident process. Its one key and one bill can cover many backend capabilities, which reduces the number of credentials and billing owners involved when the auth check also touches queues or notifications; it does not remove the need to design session policy. It is not suitable when your organization requires a provider-specific feature that is outside the verified auth operations, or when an existing platform already centralizes identity evidence. Stick with Auth0, Cognito, or Firebase when their surrounding controls are the thing you need to preserve.

Make the next page shorter

Put a synthetic check in front of this exact lifecycle: create a test account, verify a session, revoke it, and verify again. Alert on a post-revocation verification that remains accepted, and include the session ID and user relationship in the alert context without exposing tokens. Run the same check for “all devices” on a schedule.

After an incident, write down the first mismatch, not just the final fix. Was the wrong session ID selected? Did refresh happen after the user thought they had logged out? Did an edge cache outlive the policy decision? Those questions turn a vague “logout is broken” report into a bounded control that can be tested on the next deploy.

References

Top comments (0)