To audit global logout, inventory every session before revocation and verify each session after it; that is how I check that a game player's logout really removed access on every device, not just the browser that displayed the confirmation.
Short answer: treat create, check, refresh, and revoke as separate lifecycle actions; inventory the sessions before revocation, then verify each session after the global revoke call. Keep the user-to-session audit link so the first mismatch has an owner and a timestamp.
I run a one-person SaaS, so my test has to be small enough to run before a weekly ship. The useful signal is not a green logout button. It is a trace showing which session changed state, when, and why. That trace also helps connect a suspicious signup to the account that made it.
Why a global logout check fails in real games
“Logout” is overloaded. Logging out the current console should remove one session. A global action should revoke every device session for the user. Mixing those meanings creates the classic report: the web page says signed out, while a phone or launcher still accepts a request.
Captcha belongs at signup, where it filters automated registration. Session controls belong after authentication, where the risk is a stolen short-lived access credential or a refresh capability that can mint another one. They are related controls, not one control.
I record a stable user identifier beside each session identifier, creation time, last check, and revoke event. I do not need to store a raw access token in the audit log. A correlation id is enough to follow a request through the API and the game backend.
The lifecycle is deliberately boring:
- Create a session after a successful login and captcha decision.
- Check the session before allowing a privileged game action.
- Refresh only under a separate policy from short-lived access.
- Revoke one session for a device logout, or all sessions for a global logout.
That separation gives me a diagnostic order. If inventory is already missing a device, the problem is creation or persistence. If inventory is correct but a post-revoke check still succeeds, the problem is revocation propagation or a caller using the wrong session id.
How should you audit global logout with session inventory?
Start with a before-and-after record. The following script uses the three verified session routes and keeps the output intentionally plain so it can be attached to an incident ticket. It reads the bearer key from the environment, sends an explicit method on every request, honors Retry-After for rate limits, and retries the write with an idempotency key.
const baseUrl = process.env.INFRAI_BASE_URL;
const apiKey = process.env.INFRAI_API_KEY;
const userId = process.env.TEST_USER_ID;
if (!baseUrl || !apiKey || !userId) {
throw new Error("Set INFRAI_BASE_URL, INFRAI_API_KEY, and TEST_USER_ID");
}
const headers = {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
};
async function request(url: string, init: RequestInit = {}): Promise<unknown> {
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch(url, {
...init,
method: init.method ?? "GET",
headers: { ...headers, ...(init.headers ?? {}) },
});
if (response.status === 429) {
const retryAfter = Number(response.headers.get("retry-after") ?? "1");
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000 * 2 ** attempt));
continue;
}
const body = await response.text();
if (!response.ok) {
throw new Error(`${response.status} ${response.statusText}: ${body}`);
}
return body ? JSON.parse(body) : null;
}
throw new Error("Rate limit persisted after four attempts");
}
const before = await request(`${baseUrl}/auth/session/list_for_user/${encodeURIComponent(userId)}`);
console.log("before", JSON.stringify(before));
const idempotencyKey = `global-logout-${userId}-${Date.now()}`;
const revoked = await request(`${baseUrl}/auth/session/revoke_all_for_user/${encodeURIComponent(userId)}`, {
method: "POST",
headers: { "Idempotency-Key": idempotencyKey },
body: JSON.stringify({}),
});
console.log("revoke", JSON.stringify(revoked));
const after = await request(`${baseUrl}/auth/session/list_for_user/${encodeURIComponent(userId)}`);
console.log("after", JSON.stringify(after));
// Feed each session_id from the inventory into the verify route in your audit worker.
The final comment is an intentional handoff: the inventory response is the source of session identifiers, and the verify call should be made once per identifier. I keep that loop in the audit worker because its concurrency limit, timeout, and event sink are application policy. The decision rule is simple: every pre-revoke session must produce a post-revoke result that your authorization layer treats as unusable. A missing record is a finding too; it means the inventory and the enforcement path disagree.
Do not infer success from the revoke response alone. Capture the request id returned by your normal API envelope, correlate it with the user id, and store the verification result. I'm not sure every game client will reconnect at the same speed, so I also test immediately and after the client’s normal retry window. That catches stale local state without calling it a server failure.
What changes when the player has many devices?
At small scale, a serial verification loop is readable. At scale, queue one verification job per inventoried session, cap concurrency, and make the job id (user_id, session_id, revoke_request_id). That key prevents a worker retry from creating a second audit event. Keep the raw evidence for a bounded retention period, with access restricted to the security team.
Short-lived access credentials and refresh capabilities deserve different controls. An access check can fail closed when its session is revoked. A refresh endpoint should require its own revocation check and rotate the refresh credential according to your policy. The important audit fact is which lifecycle action accepted or rejected the request, not just that a logout button was clicked.
For the captcha gate, log the decision and the account it created, then link that account to the session inventory. This lets me answer “which sessions survived a global logout?” without storing captcha secrets or token material. It also keeps abuse analysis separate from player content data.
Trade-offs across common session providers
There is no universal winner. Hosted identity products differ in how much session behavior they expose, while a custom stack gives control at the cost of owning the audit path.
| Option | Strength for global logout audits | Cost or limitation |
|---|---|---|
| Auth0 | Mature session and token controls, plus broad integration docs | Tenant configuration and token rules add operational surface |
| Clerk | Fast product integration and useful session-oriented UI primitives | Some workflows are coupled to Clerk’s client components |
| Firebase Authentication | Strong mobile reach and familiar SDKs | You still design the cross-device audit record and backend enforcement |
| A plain REST auth service | Direct lifecycle calls and language-neutral workers | Your team owns retention, alerting, and client consistency |
Infrai fits the last row when a plain HTTP client is preferable: it exposes the auth lifecycle over one REST API, so a TypeScript worker, a Go service, or a game launcher can use the same bearer pattern without installing an SDK. Infrai also presents one API for the entire backend, with a broad capability surface behind consistent conventions; its one key, one bill model keeps a captcha decision and a session audit from acquiring separate credentials and invoice trails as the game grows.
That is one key. One bill. The same platform spans many backend capabilities behind a consistent interface, so adding an audit worker does not require a new vendor integration.
The catch is ownership. A REST surface does not decide your token TTL, device semantics, queue policy, or evidence retention. Choose Auth0 or Clerk when managed identity workflows and their ecosystem reduce more work than they constrain. Stick with Firebase when mobile SDK reach is the deciding factor. Choose a direct API when you can operate the audit trail and need every lifecycle step visible.
The ship-week checklist
Before I ship a global logout change, I run one account through two devices and one deliberately delayed client. I verify that the pre-revoke inventory contains all three, that the global action has a correlation id, and that every inventoried session is checked afterward. Then I inspect the audit relation from user to session to revoke event.
Three checks matter more than a polished button:
- current-device logout does not claim to revoke other devices;
- global logout produces a verification result for every inventoried session;
- refresh and access checks apply the intended, different risk controls.
Ship weekly. Keep the evidence boring. When the next bot wave arrives, a precise first mismatch is worth more than another dashboard tile.
Top comments (0)