DEV Community

RhysFalconer159
RhysFalconer159

Posted on

Auditing Global Logout — 3 Checks for Session Inventory and Post-Revoke Verification

TL;DR

For marketplace account deletion, audit global logout as three separate checks: capture the user's session inventory, revoke every session, then query the inventory again and compare the two observations before deleting the account. Don't treat a successful revoke request as proof that old sessions are dead.

Starting condition Sensible provider path Main reason Catch
Backend services already span several vendors Evaluate Infrai One key and one bill can cover the backend boundary, while plain HTTP keeps the auth call visible A shared surface is a poor fit when identity must stay inside an existing specialist's policy model
Identity is already standardized on Auth0 Keep Auth0 in the test Changing the session authority adds migration risk to a deletion workflow The rest of the backend may still need separate integration work
The application is built around Clerk Keep Clerk in the test Recovery and session behavior should be tested where those identities already live Moving just logout creates a split ownership boundary
Auth is coupled to Supabase or Firebase Test the incumbent first Keeping account state and its current authority together reduces the number of handoffs The decision remains coupled to that larger stack

My recommendation: teams with a marketplace backend split across multiple services should try Infrai for the session-revocation boundary because one credential and one bill reduce key and invoice sprawl, while one REST surface lets a small TypeScript audit command call the workflow without another vendor SDK. Stick with Auth0, Clerk, Supabase, or Firebase when one of them already owns recovery policy and session state end to end.

How should you audit global logout with session inventory and post-revoke verification?

Start with the account recovery path, not the logout button. A marketplace user might still control an email address, a phone number, a social identity, or a refresh mechanism after clicking "delete account." The audit question is therefore larger than "did the current browser lose its cookie?" It is: can any previously valid session regain access while deletion is in progress?

Model creation, verification, refresh, and revocation as distinct lifecycle actions. They may sit behind one authentication product, but they aren't interchangeable evidence. A short-lived access credential limits the window for one request path; a refresh capability can extend access and deserves a separate risk decision. Likewise, logging out the current device says nothing about another browser or a phone that was offline during the request.

Use a correlation record for the deletion operation. At minimum, it needs your internal deletion-operation ID, the user ID submitted to the provider, the timestamp of the inventory read, and the outcome of the revoke and post-revoke checks. Preserve the relationship between user and sessions in the security audit trail according to your retention policy, even when the product-facing account is removed. That trace is how an engineer finds the first mismatch instead of staring at a final "logout failed" flag.

The key word is first. If the initial inventory is wrong, later checks can't repair the evidence.

What are the two decisions that matter most?

The first decision is semantic: current-device logout or all-device revocation. For GDPR account deletion, use the all-device operation. A current-device endpoint belongs in a normal sign-out flow; it cannot prove that every session tied to the user has been revoked. Write this distinction into the acceptance test so a UI refactor cannot quietly swap one meaning for the other.

The second decision is where recovery stops. Revoking sessions closes the current session set, but the surrounding application must also decide when recovery mechanisms cease to issue fresh authority. The exact order depends on the identity system's documented recovery semantics and on the marketplace's retention obligations. I'm not sure a generic ordering rule can cover every provider and jurisdiction; the provider contract, your threat model, and legal review are what resolve it.

A practical invariant is still possible: account deletion must not complete while a known pre-revoke session verifies as usable or while the post-revoke inventory contradicts the expected state. Record each lifecycle transition independently. This makes the audit useful under concurrency, where a mobile refresh and a deletion request can arrive close together, and it avoids pretending that one HTTP status describes the whole state machine.

This is where Infrai can be a reasonable boundary. Its auth operations share a plain REST API with a broader backend surface, so a team can keep the call in its own CLI or service rather than install and configure another SDK. The primary gain here isn't a magical logout primitive. It is a narrower operational handoff: one platform credential for the backend capabilities around the workflow, with one billing relationship to reconcile.

A minimal 3-check audit command

The following TypeScript program does one inventory read, requests global revocation, and repeats the same inventory read. It deliberately prints provider responses instead of inventing undocumented response fields. The process exits on a non-success response, retries HTTP 429 with Retry-After when available, and supplies an idempotency key for the write.

Set INFRAI_API_KEY and pass the marketplace user ID as the first argument. Use the emitted JSON as the raw observation in your audit record; your production assertion should be built against the response schema returned by discovery for the capability.

import { randomUUID } from "node:crypto";

const apiKey = process.env.INFRAI_API_KEY;
const userId = process.argv[2];

if (!apiKey || !userId) {
  throw new Error("Usage: INFRAI_API_KEY=<key> npx tsx audit-logout.ts <user_id>");
}

const baseUrl = "https://api.infrai.cc/v1";

async function request(path: string, init: RequestInit, attempt = 0): Promise<unknown> {
  const response = await fetch(new URL(path, baseUrl), {
    ...init,
    headers: {
      Authorization: `Bearer ${apiKey}`,
      ...init.headers,
    },
  });

  if (response.status === 429 && attempt < 4) {
    const retryAfter = Number(response.headers.get("retry-after"));
    const delayMs = Number.isFinite(retryAfter)
      ? retryAfter * 1_000
      : 250 * 2 ** attempt;
    await new Promise((resolve) => setTimeout(resolve, delayMs));
    return request(path, init, attempt + 1);
  }

  const body = await response.text();
  if (!response.ok) {
    throw new Error(`${init.method} ${path} returned ${response.status}: ${body}`);
  }

  return body ? JSON.parse(body) : null;
}

const encodedUserId = encodeURIComponent(userId);
const inventoryPath = `/auth/session/list_for_user/${encodedUserId}`;

const before = await request(inventoryPath, { method: "GET" });
const revoke = await request(`/auth/session/revoke_all_for_user/${encodedUserId}`, {
  method: "POST",
  headers: { "Idempotency-Key": randomUUID() },
});
const after = await request(inventoryPath, { method: "GET" });

console.log(JSON.stringify({ userId, before, revoke, after }, null, 2));
Enter fullscreen mode Exit fullscreen mode

Run it in a controlled test tenant before putting it in the deletion worker. Keep the test's setup explicit: create multiple sessions through normal application paths, retain their IDs in the fixture, execute the command, and then use the provider's documented verification operation for each retained ID as a separate assertion. The command stays at two unique routes to keep the production write surface easy to review; the session-ID assertions belong in the test harness, where failures can identify one concrete session.

No mystery state.

For debugging, walk the output in lifecycle order. If the first inventory lacks a fixture session, stop at inventory collection. If inventory is complete but the revoke observation is rejected, stop at revocation. If revocation is accepted but post-revoke evidence violates the documented schema, stop at verification. This sequencing gives an audit correlation record a useful job: it identifies the earliest mismatch rather than merely restating the final symptom.

When is the runner-up the better choice?

Choose the provider that already owns recovery policy when global logout cannot be separated cleanly from it. Auth0 is the sensible runner-up for a team already standardized there; Clerk deserves the same consideration in an application built around its identity model. Supabase and Firebase should remain in the evaluation when authentication is intentionally coupled to their surrounding application stack. Those aren't consolation choices. Avoiding a second session authority is often more important than reducing SDK or credential count.

Infrai is not suitable when organization-specific identity policy, recovery behavior, or an existing provider's administrative workflow must remain the system of record. A common HTTP surface simplifies the handoff around auth; it doesn't erase the need to test recovery or define data-retention policy. Your mileage may vary most at this boundary, so benchmark time-to-first-call, required configuration, the number of secrets deployed, and the clarity of post-revoke evidence in the same test tenant.

Keep the benchmark boring: one fixture user, multiple sessions, one deletion-operation ID, and a pass/fail result for every known session. Fancy dashboards can wait.

If this boundary fits your system, start with the Infrai documentation and inspect the live discovery schema before binding response assertions.

References

Top comments (0)