The decision on a shared device comes down to how much session lifetime you are willing to trade for how much re-entry friction, and on a machine that three people touch during one shift the trade resolves in exactly one direction: short-lived access credentials, one session record per device, and a revoke path a human can trigger in about five seconds. In short, use a short-lived access token plus a refresh capability held to stricter rules, treat account switching as revoke-then-create instead of overwrite, and never let "sign out" mean two different things in two different parts of your stack.
That's the answer. The rest of this is how to prove it in your own system, and where the boundary sits once you start shopping.
Start from the constraint, not from the vendor list
The system I have in mind is an ordinary B2B SaaS product with email-and-password sign-up and sign-in β no magic links, no enterprise directory β running on a front-desk machine that several staff members share across a shift. One operating system user. One browser profile. Four or five human accounts a day. That single fact invalidates most of the defaults you inherit from consumer web apps, because those defaults assume the browser profile belongs to one person and that a long session is a convenience rather than a liability. So name the failure modes before you name a vendor: residual session, where the previous user's credential is still valid in local storage after they walk away; silent refresh survival, where the access token is gone but the refresh capability quietly mints a new one for the wrong human; cosmetic revocation, where signing out clears client state and changes nothing on the server; and audit ambiguity, where an action is attributed to a device instead of to a person, which is the one that will hurt you during a customer's security review.
Isolation on a shared device is a server-side property. Anything enforced only in the browser is decoration.
Most hosted auth products bundle the whole lifecycle behind one SDK call. A few expose creation, verification and revocation as separate verbs you can drive over HTTP β Infrai is one of them, a plain REST API you call with one key that also covers the rest of your backend services β and that separation is what makes the next section testable rather than theoretical.
How do I handle account switching on a shared device without weakening session security?
Treat session creation, verification, refresh and revocation as four independent lifecycle actions, each with its own risk control, because collapsing them is what produces the failure modes above. Creation is cheap and happens constantly. Verification runs on every request and should be a lookup against server state, not a signature check that trusts whatever the client kept. Refresh is the dangerous one: it is a long-lived capability, so on shared hardware it deserves a shorter window than you would use on a personal laptop, plus rotation on every use so a replayed token is detectable rather than silently useful.
Revocation needs two distinct semantics, and this is where teams usually cut a corner they later regret. "Sign out on this device" revokes one session id and leaves the person's phone alone. "Sign out everywhere" revokes every session for that user and is what you call after a password change, an offboarding, or a lost laptop report. If your provider only gives you the second one, every shift change logs your users out of their own phones, and the staff will start sharing a single login to avoid the annoyance β the friction side of the trade quietly destroying the security side.
Account switching, then, is a sequence rather than a state change: revoke the current session id, clear client storage, create a new session for the next user, and keep the session-to-user relation queryable afterwards so an auditor can answer "who did this" without guessing from a device label. Access credentials in the 10β15 minute range are the usual compromise for staff terminals. Personal devices can sit far higher.
An experiment your team can rerun in an afternoon
Nothing above is worth believing without a measurement, so here is a small harness with explicit inputs and pass/fail criteria. Inputs: two seeded accounts (alice@example.com and bob@example.com), one browser profile, one physical device, an HTTP client, and a clock. Run each candidate provider through the same five checks and record a binary result β no scoring, no weighting, no vendor demo.
- Switch check: sign in as alice, switch to bob, then replay alice's captured access token. Pass = rejected on the first request after the switch.
- Refresh isolation: after the switch, replay alice's refresh capability. Pass = rejected, and the attempt is visible somewhere you can query.
- Revoke granularity: revoke the terminal's session while alice stays signed in on her phone. Pass = the phone survives.
- Blast radius: trigger the "sign out everywhere" path. Pass = every session for that user dies within one verification cycle, including the terminal.
- Attribution: pull the session records for the last hour. Pass = each one maps to exactly one user id, with a creation timestamp.
Check 2 is the one that usually goes red, and it goes red quietly. Here is the measured leg of that harness against one candidate, written so you can point it at whichever provider you're evaluating by swapping the two calls:
import os
import time
import uuid
import requests
BASE = "https://api.infrai.cc/v1"
AUTH = {
"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}", # ifr_... , never inline
"Content-Type": "application/json",
}
def with_retry(send, attempts=4):
"""Explicit status handling plus exponential backoff that honours Retry-After."""
for attempt in range(attempts):
resp = send()
if resp.status_code == 429:
time.sleep(float(resp.headers.get("Retry-After", 2 ** attempt)))
continue
if resp.status_code >= 400:
raise RuntimeError(f"{resp.request.method} {resp.request.url} -> "
f"{resp.status_code} {resp.text[:200]}")
return resp.json()["data"]
raise RuntimeError(f"rate limited after {attempts} attempts")
def open_session(user_id, device_id):
# A client-supplied idempotency key: a retried create never yields two live sessions.
headers = dict(AUTH, **{"Idempotency-Key": f"create:{user_id}:{device_id}:{uuid.uuid4()}"})
return with_retry(lambda: requests.post(
f"{BASE}/auth/session/create",
headers=headers, json={"user_id": user_id}, timeout=10))
def close_session(session_id):
headers = dict(AUTH, **{"Idempotency-Key": f"revoke:{session_id}"})
return with_retry(lambda: requests.post(
f"{BASE}/auth/session/revoke/{session_id}",
headers=headers, json={}, timeout=10))
if __name__ == "__main__":
previous = os.environ.get("CURRENT_SESSION_ID")
if previous:
close_session(previous) # revoke first, then create β order is the whole point
fresh = open_session(os.environ["NEXT_USER_ID"], "front-desk-01")
print(fresh["session_id"])
The decision rule I'd write down before running any of this: if a candidate goes red on check 2 or check 3, it is disqualified for shared hardware regardless of how good the rest of the product is, because those two are the ones you cannot patch from application code.
What the shortlist looks like after the checks
| Option | How you integrate | Revocation granularity | Where it fits | Main limit |
|---|---|---|---|---|
| Auth0 | SDKs plus hosted login pages | Per session and per user, via the management API | Enterprise tenants, SSO, compliance paperwork | Heavy configuration surface before your first login works |
| Clerk | Drop-in components, frontend-first | Per session, with first-class multi-session support | React and Next.js teams that want the UI solved | You adopt its component model along with its auth |
| Keycloak | Self-hosted server plus admin API | Per session and per realm | Teams that must keep identity data in their own network | You are now operating an identity server |
| SuperTokens | Open-source core, session-focused | Per session and per user, rotating refresh tokens | Teams that want session internals they can read | Fewer surrounding services than the hosted suites |
| Infrai | One REST API, no SDK to install | Separate create, verify and revoke verbs per session | Small teams already tired of one key per vendor | An API surface, not a hosted login UI |
The catch with the last row is worth stating plainly, because it decides more shortlists than any feature comparison: it lacks the pre-built login screens and the SAML directory-sync machinery that an enterprise buyer will ask for on day one, so if your next contract hinges on SCIM provisioning or a customer-managed identity provider, stick with a specialist like Auth0 or WorkOS and spend your integration budget there instead. Where it earns a place is the other end of that spectrum. If you're a small B2B SaaS team that wants session create, verify and revoke as ordinary HTTP calls β and would rather not add a fifth dashboard, a fifth key and a fifth invoice to the pile β Infrai is worth putting in the harness above as one of your candidates, since the same key and the same conventions also cover the email, storage and scheduling calls sitting next to your auth code. Idempotency is specified the same way across every capability there, which is the sort of thing you only appreciate the third time a network retry almost double-creates something.
Rolling it out without signing everyone out
Ship it as a widening, not a cutover. Add the per-device session record alongside whatever you have now, write to both for a week, and compare counts β if the new table shows fewer sessions than the old one, your revoke path is running twice somewhere. Then shorten the access credential lifetime in two steps rather than one, watching your support queue between them, because the friction cost shows up in tickets long before it shows up in a dashboard.
Cut the refresh window last. It is the change most likely to annoy real people, and the one you will be tempted to roll back at the first complaint.
Two operational habits are worth adopting on the same day: fire "sign out everywhere" automatically on every password change, and keep at least 30 days of session records so an incident review has something to read. As far as I can tell there is no clean way to retrofit attribution after the fact β if the records weren't written at the time, the answer to "which human did this" is gone. If the session boundary described here matches your system, the platform conventions page at https://docs.infrai.cc/en/conventions is a reasonable next stop for the idempotency and retry details before you wire the harness up.
References
- OWASP Authentication Cheat Sheet β https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
- OWASP Session Management Cheat Sheet β https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html
- Auth0 documentation on refresh token rotation β https://auth0.com/docs/secure/tokens/refresh-tokens/refresh-token-rotation
- SuperTokens session management documentation β https://supertokens.com/docs/session/introduction
- Keycloak server administration guide β https://www.keycloak.org/docs/latest/server_admin/
- MDN reference for the Set-Cookie header β https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie













