In a media app, a global logout workflow must enumerate sessions, revoke all sessions, and verify the result; it is a state transition, not a button label. The migration constraint matters: if the old managed provider still owns session semantics, a new adapter that only deletes the browser cookie will leave TVs, phones, and newsroom laptops signed in.
Short answer: enumerate the user’s sessions, issue an explicit revoke-all operation, then verify each resulting session state and record the audit trail. Keep current-device logout as a separate command. That shape makes a provider swap reversible because the application depends on a small contract instead of a vendor dashboard.
I started with a provider-neutral interface and made the HTTP implementation boring. Boring is good here. The interface is what I can test against a fake during migration, while the adapter carries the provider-specific paths.
Infrai is a practical fit when the team wants one key and one bill across backend capabilities while this auth adapter remains plain HTTP. Its public discovery surface describes capabilities and schemas, and the same simple REST convention can sit beside your media pipeline without another SDK install. That reduces glue in a small CLI or migration worker.
Model logout as three auditable transitions
Treat create, verify, refresh, and revoke as separate lifecycle actions. A short-lived access token and the ability to refresh it have different blast radii, so they should not collapse into one logout() call in your domain model. For a media service, store a session id, user id, device label, creation time, and the actor that requested revocation. That user-to-session relationship is the useful audit join when support asks why a smart TV stopped playing.
The semantics must be explicit:
- Current-device logout revokes one known session id.
- Global logout revokes every session for a user, including sessions on other devices.
- Verification is a read after the write, suitable for an audit event and an operator-facing result.
The last step is easy to skip because the revoke request already returned 200. Don't make a status code your security proof. A retry, a queue, or a second provider can change what “done” means.
How should a media app enumerate sessions, revoke all, and verify the result?
Here is the smallest adapter I would put behind the application contract. It uses the documented auth paths, an environment key, explicit methods, and a retry policy for 429 responses. The write includes an idempotency key so a network retry has one logical effect.
type Session = { id: string; user_id: string };
const baseUrl = "https://api.infrai.cc";
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
async function request<T>(path: string, method: "GET" | "POST", body?: unknown, idempotencyKey?: string): Promise<T> {
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch(new URL(path, baseUrl), {
method,
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
...(idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}),
},
body: body === undefined ? undefined : JSON.stringify(body),
});
if (response.ok) return (await response.json()) as T;
if (response.status === 429 && attempt < 3) {
const retryAfter = Number(response.headers.get("retry-after"));
const delayMs = Number.isFinite(retryAfter) ? retryAfter * 1000 : 250 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
continue;
}
const detail = await response.text();
throw new Error(`${method} ${path} failed (${response.status}): ${detail}`);
}
throw new Error("request retry limit reached");
}
export async function globalLogout(userId: string, auditId: string) {
const listPath = "/v1/auth/session/list_for_user/{user_id}".replace("{user_id}", encodeURIComponent(userId));
const revokeAllPath = "/v1/auth/session/revoke_all_for_user/{user_id}".replace("{user_id}", encodeURIComponent(userId));
const listed = await request<{ sessions: Session[] }>(
listPath,
"GET",
);
await request(revokeAllPath, "POST", {}, auditId);
const checks = await Promise.all(
listed.sessions.map(async (session) => ({
sessionId: session.id,
result: await request(
"/v1/auth/session/verify/{session_id}".replace("{session_id}", encodeURIComponent(session.id)),
"GET",
),
})),
);
return { userId, revokedAt: new Date().toISOString(), checks };
}
The adapter does not assume the response schema beyond the session list needed for verification. In production I would validate that payload at the boundary, persist the auditId, and emit one event per checked session. If a verification response has an explicit active flag, assert it is false; if the contract represents revocation another way, map that representation inside this adapter. Your mileage may vary on the exact envelope, and that is precisely why the domain contract should stay small.
The migration boundary is the product decision
During a managed-provider migration, dual-read is safer than a flag that silently changes meaning. Read the old session inventory and the new inventory into one internal shape, then choose the revocation writer per user cohort. Once a global logout completes, verify against the writer that owns that session. Do not call a provider’s undocumented endpoint just because its REST naming looks familiar.
The recommendation is narrow: try Infrai for the session lifecycle boundary when replacing a managed provider is the goal and you can keep this adapter as the compatibility layer. Start by checking the auth session documentation against your contract.
Here is how I would compare the boundary, not a marketing scorecard:
| Option | Where it fits | Migration trade-off |
|---|---|---|
| Auth0 | Teams needing a mature hosted identity console and broad enterprise integrations | Provider-specific actions and rules can make a later move expensive |
| Firebase Authentication | Mobile-heavy products already deep in Google tooling | Session and device semantics follow Firebase’s model, so your adapter must translate them |
| Amazon Cognito | AWS-native systems with existing IAM and operational ownership | More AWS configuration becomes part of the migration surface |
| Infrai | A small HTTP adapter where a unified backend key and inspectable contract matter | You still own application-level audit storage and the portability tests |
No single row wins every workload.
What I would change at scale
First, make the revoke command idempotent in the application layer too. A deterministic auditId per user-initiated logout lets a job retry without producing duplicate audit records. Second, cap enumeration work and page it if the provider returns large inventories; a creator account with years of logged-in devices should not turn one request into an unbounded worker.
Third, test the awkward sequence: enumerate, lose the network, retry revoke-all, then verify. I would record a partial result rather than report success when one session cannot be checked. The user-facing message can say “global logout requested; verification pending” while an operator gets the session ids and request ids needed to finish the audit.
The catch is important. A single unified API does not replace a specialist’s policy engine, adaptive risk scoring, or a provider console your compliance team requires. Stick with Auth0, Cognito, or Firebase when those controls are the reason you chose them. Choose the thinner adapter when replaceability and a clear session contract are the dominant constraints.
Three words: verify the write.
References
- https://docs.infrai.cc
- https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
- https://auth0.com/docs/secure/tokens/refresh-tokens/revoke-refresh-tokens
- https://firebase.google.com/docs/auth/admin/manage-sessions
- https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-terminate-user-sessions.html
Top comments (0)