Treat an account switch as a security-boundary change, not a navigation event. For a household marketplace app on a shared tablet, the operational rule is: revoke the old server-side session, clear every account-scoped client cache, create a fresh session only after explicit authentication, and record the transition without recording secrets.
This matters most around password recovery. A recovery link opened on the family tablet can otherwise inherit the previous shopper's cart, addresses, messages, or anti-abuse state. The page may display the new name while a background request still carries the old cookie. That is a cross-account disclosure with a friendly-looking screen.
The safe default is fail closed.
What signal turns an ordinary switch into an abuse investigation?
The first signal is a mismatch between the identity rendered by the client and the identity authorized by the server. Never let an account_id supplied by the browser settle that dispute. Resolve authorization from the opaque session token on every account-scoped request, then bind the resulting subject to the resource being read or changed. OWASP's authentication guidance also recommends generic authentication responses so an attacker cannot use recovery behavior to enumerate registered accounts.
For a marketplace, watch the whole transition rather than a single login result. A useful event chain is recovery_requested, recovery_completed, session_revoked, account_authenticated, and switch_completed, all tied together with a random correlation ID. Store the actor subject after authentication, the target subject only where policy permits it, the device or client identifier, outcome, reason code, and timestamps. Don't log passwords, recovery tokens, raw cookies, or full authentication responses.
The abuse case is subtle: somebody with temporary access to a kitchen tablet requests several resets, opens one from shared email, then switches accounts repeatedly to probe saved addresses or seller messages. Rate limits should therefore apply on more than one dimension. Use coarse limits for the source network, stricter limits for the device, and an account-level limit whose response remains generic. Exact thresholds depend on traffic and false-positive tolerance; I'm not sure a universal number exists. A load test plus a week of representative, privacy-reviewed telemetry will resolve that question better than copying somebody else's constant.
Recovery traffic also needs queue discipline. Sending the message asynchronously is reasonable, but the public response must not reveal whether an account exists, and retries must not mint a new usable token each time. Give the request an idempotency key, persist one logical recovery operation, and make delivery retries refer to that operation. Duplicate delivery should be harmless. A scheduler retry is not permission to create another credential.
How should shared-device authentication preserve session isolation during safe account switching?
Use a short, explicit state machine. Authenticated(A) may move to SwitchPending only after the server invalidates A's session. SwitchPending owns no account data. It may move to Authenticated(B) after B completes authentication, including any required additional factor. Cancellation returns to a signed-out state, not to A. This creates a small interruption for the person holding the device, but it removes an ambiguous state in which two principals appear active.
The server is authoritative. Browser storage can improve presentation, but it can't authorize a cart mutation, an address read, or a seller payout view. Namespace harmless local preferences by a stable, non-secret subject identifier, and delete account-scoped query caches, optimistic updates, service-worker responses, WebSocket subscriptions, and in-memory stores at the switch boundary. A full document reload after revocation is often the clearest implementation. It's a little slower. It is also easy to reason about during an audit. The recovery token deserves its own lifecycle: generate it with a cryptographically secure random source, store only a verifier or digest, set an expiry, bind it to one purpose and one account, and consume it once. After a successful password reset, offer the user a deliberate choice about invalidating other sessions; the policy should match the threat model and the behavior described to the user. Recovery completion must never silently authenticate whichever account happened to be visible before the link opened.
The following Go sketch keeps the boundary in one server-side operation. Storage details are abstract so the invariant is visible; a production implementation must make Revoke atomic in its backing store and protect the endpoint against cross-site request forgery when cookie authentication is used.
package auth
import (
"crypto/rand"
"encoding/base64"
"net/http"
)
type Sessions interface {
Subject(token string) (string, error)
Revoke(token string) error
}
type Audit interface {
Record(event, subject, correlationID, outcome string)
}
type Handler struct {
Sessions Sessions
Audit Audit
}
func correlationID() (string, error) {
b := make([]byte, 18)
if _, err := rand.Read(b); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(b), nil
}
func (h Handler) BeginSwitch(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("session")
if err != nil {
http.Error(w, "authentication required", http.StatusUnauthorized)
return
}
subject, err := h.Sessions.Subject(cookie.Value)
if err != nil {
http.Error(w, "authentication required", http.StatusUnauthorized)
return
}
id, err := correlationID()
if err != nil {
http.Error(w, "request could not be completed", http.StatusServiceUnavailable)
return
}
if err := h.Sessions.Revoke(cookie.Value); err != nil {
http.Error(w, "request could not be completed", http.StatusServiceUnavailable)
return
}
http.SetCookie(w, &http.Cookie{
Name: "session",
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode,
})
h.Audit.Record("session_revoked", subject, id, "success")
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(http.StatusNoContent)
}
Keep the next login separate from BeginSwitch. Combining revocation and login in one request encourages partial-success semantics: B's credentials can fail after A has been revoked, or A can remain live because the code tried to preserve a convenient rollback. The correct intermediate state is signed out. B receives a newly generated session identifier after authentication; the old identifier is never upgraded or reassigned.
Make the audit trail useful without making it dangerous
An auditor needs to reconstruct who crossed which boundary, when, and under which policy. Operations needs enough signal to distinguish a forgotten password from automated probing. Neither group needs the bearer credential.
Use structured, append-oriented security events with a documented schema. Separate the public outcome from the internal reason: the caller can receive the same recovery response for known and unknown addresses while an access-controlled event records subject_not_found, rate_limited, or accepted. Protect logs against modification, restrict access, define retention, and test that redaction happens before serialization. OWASP's logging guidance is a practical checklist for these controls.
A compact decision table keeps reviews focused:
| Boundary | Required invariant | Evidence to retain | Never retain |
|---|---|---|---|
| Recovery request | Public response does not disclose account existence | Correlation ID, coarse source signal, policy outcome | Email content, recovery token |
| Recovery completion | Token is purpose-bound, expiring, and single-use | Operation ID, subject ID, completion time | New password, token verifier |
| Account switch | Old session is revoked before new login | Old subject, correlation ID, revocation outcome | Raw session cookie |
| New login | New session ID belongs only to the authenticated subject | New subject, authentication method class, time | Factor secret or challenge answer |
The catch is storage and response latency. Synchronous revocation requires a session store that can enforce invalidation immediately, and detailed security events increase retention and access-control work. This pattern is not suitable when an offline-first application must switch profiles with no server contact; use isolated local OS profiles or separate encrypted application vaults there, and make the offline trust boundary explicit. For a connected household marketplace handling messages, addresses, and recovery links, accepting a brief signed-out interval is the more defensible trade.
Verification, deployment, and rollback
Test the transitions, not just the pages. Start with two accounts, A and B, on one browser profile. Warm every cache while signed in as A, open a live update channel, begin a recovery for B, switch, and then assert that A's cookie, cached responses, subscriptions, back-button history, and queued mutations cannot read or change A's resources. Repeat with two tabs because a broadcast race is easy to miss. Then replay the old cookie directly against the server; the UI result proves very little.
Add property-style tests around the core invariant: after revocation succeeds, no request bearing the old token is authorized, regardless of retry order. Exercise double-clicks on the switch control, duplicated queue delivery, an expired recovery token, reuse of a consumed token, logout in another tab, clock skew at expiry, and a client that closes between revocation and cache cleanup. For each case, assert both the authorization result and the audit event. No event is a failed test, not a logging footnote.
Deploy behind a server-enforced policy flag and compare counts of switch starts, successful fresh authentications, rate-limit decisions, and abandoned signed-out states. Alert on impossible sequences, such as switch_completed without a preceding successful authentication under the same correlation ID. Track latency for session revocation separately from login latency so an operational regression has an owner.
Rollback needs care. You can roll back a new screen or stricter rate-limit threshold, but don't restore the behavior that reassigns an existing session from A to B. Keep the old sessions revoked. If the new login path must be disabled, leave users signed out and route them through the established authentication path. Availability pressure doesn't justify weakening the identity boundary; record the decision, time-box the exception if one is unavoidable, and require security review.
Before release, make the runbook answer five questions in plain language: how to revoke a subject's sessions, how to find a switch by correlation ID, how to distinguish abuse from delivery delay, who can read the security log, and what operators do when revocation latency breaches its objective. The resulting document is audit evidence and an incident tool. It also forces unresolved ownership into the open before a late-night escalation does.
References
- https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
- https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html
- https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html
- https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html
- https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html
Top comments (0)