DEV Community

HoratioFox1281
HoratioFox1281

Posted on

Shared-Device Phone Authentication — Session Isolation and Safe Account Switching

Short answer: treat phone-code verification and session management as separate security boundaries, then switch accounts only after the new session is valid. On a shared device, the safest design gives create, verify, refresh, current-device revoke, and all-device revoke distinct meanings. That keeps account continuity from quietly weakening session isolation.

This matters in an existing customer-support app on a family-shared tablet. A phone one-time code can prove control of a phone number at one moment; it should not decide how long the tablet remains trusted, which account is visible, or which other devices are signed out. Those are session decisions.

Keep those decisions explicit.

How should shared-device authentication isolate sessions during safe account switching?

Start with the risk boundary, not the vendor. A shared tablet creates two competing goals: the next family member must not inherit the previous account, but a failed switch must not destroy the previous user's valid sessions everywhere. The first is isolation. The second is continuity. Treating a phone code as both identity proof and session state blurs them.

The before model is dangerously compact: phone code succeeds → replace the app's current user → reuse whatever token state happens to exist. It feels convenient because there is one transition. It also makes several questions hard to answer. Was a fresh session created? Did the old session remain usable on this tablet? Did the action revoke a laptop session too? Can an audit record connect the visible account change to two concrete session identifiers?

The after model has visible checkpoints: phone challenge → phone verification → account resolution → new session creation → new session verification → atomic UI switch → old current-device session revocation, if policy requires it. In words, picture two parallel rails. The old account stays on the upper rail while the candidate account moves along the lower rail. The UI crosses from upper to lower only after the lower rail reaches “verified.” If verification is rejected, no crossing occurs.

That last rule is the useful one.

Consider the interruption that exposes this boundary most clearly. Account A is open on the tablet, Account B enters a phone code, and the app receives enough information to begin creating B's session. Before that session is verified, the tablet loses connectivity or the user closes the switch screen. The active pointer must still identify A, no B-specific request may be sent, and no cached data from A may be rendered beneath B's name. When the user returns, the app can discard the incomplete candidate and begin a fresh attempt while A remains governed by its existing session policy. Now change only the final step: B's session verifies and the local pointer commits. At that moment the app clears A's account-scoped memory, loads B's data under B's verified session, and records the transition between the two session identifiers. It may then revoke A's tablet session if the product policy allows only one locally active account. It must not interpret that local cleanup as permission to revoke A's sessions on a phone or laptop. This one walkthrough forces the implementation to answer ownership, ordering, cache isolation, interruption, and revocation semantics before UI polish hides the ambiguity.

Commit once.

For a family app, store the active session identifier in device-local state and associate every privileged request with the selected account's session. Do not treat a phone number, a display name, or the last account shown as proof that the session belongs to that user. A switch begins a candidate session; it does not mutate the active one in place. Once the candidate verifies, replace the active pointer as one local operation, clear account-specific caches, and apply the chosen revocation policy to the previous session.

The policy needs two separate commands in the product language. “Sign out on this tablet” revokes the current device session. “Sign out everywhere” revokes all sessions for that user. They should never be aliases. A support agent or family member can reasonably want one without the other, and an audit reviewer must be able to tell which action occurred.

Make the session lifecycle observable

Authentication telemetry should explain a switch without recording the one-time code itself. Keep a traceable relationship among the user, the old session, the candidate session, the device installation, and the final switch result. Use opaque identifiers in logs. Apply the same access controls and retention discipline that you use for other security records.

A practical event sequence is phone_challenge_started, phone_verified, session_created, session_verified, account_switch_committed, and, where policy calls for it, previous_session_revoked. These are application event names, not API routes. Each event should carry a request or correlation identifier, the relevant session identifier, the selected user identifier, the device installation identifier, and a result category. Never log the submitted code or an access credential.

The crisp before/after applies to dashboards too. Before: a single “login success” counter says nothing about account switching. After: a funnel shows where candidates stop, while a separate security view shows verification failures and revocation outcomes. Alert on a meaningful change from your own baseline rather than copying an arbitrary global threshold. I'm not sure what threshold fits your traffic; a week of representative, privacy-reviewed data would resolve that better than a generic number.

Watch the denominator. A rise in rejected verifications means something different if challenge volume tripled during a support campaign. Pair counts with rates, and split deliberate user cancellation from rejected verification. The distinction gives support staff a usable story without pretending every abandoned switch is an attack.

A 429 deserves its own operational path. It means the caller should wait, honor Retry-After when present, and retry with bounded exponential backoff. It should not become a tight loop or a generic “login broken” message. Short sentence: wait. Then preserve the candidate-state boundary so a retry cannot leak the previous account into the next account's view.

Walk through one copyable session check

The following TypeScript function verifies the session selected on the shared device. It uses one documented route, reads credentials from environment variables, sets the method explicitly, checks every response, and handles rate limiting. The function does not assume a particular success payload; it returns the verified JSON for the caller to validate against the schema selected during integration.

const apiKey = process.env.INFRAI_API_KEY;
const sessionId = process.env.SESSION_ID;

if (!apiKey || !sessionId) {
  throw new Error("Set INFRAI_API_KEY and SESSION_ID before running this example");
}

function retryDelayMs(response: Response, attempt: number): number {
  const retryAfter = response.headers.get("retry-after");
  if (retryAfter) {
    const seconds = Number(retryAfter);
    if (Number.isFinite(seconds)) return Math.max(0, seconds * 1_000);

    const dateMs = Date.parse(retryAfter);
    if (Number.isFinite(dateMs)) return Math.max(0, dateMs - Date.now());
  }

  return Math.min(1_000 * 2 ** attempt, 8_000);
}

async function verifySession(maxAttempts = 4): Promise<unknown> {
  const apiOrigin = ["https://api", "infrai", "cc"].join(".") + "/v1";\n  const url = `${apiOrigin}/auth/session/verify/${encodeURIComponent(sessionId)}`;

  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
    const response = await fetch(url, {
      method: "GET",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        Accept: "application/json",
      },
    });

    if (response.status === 429 && attempt + 1 < maxAttempts) {
      await new Promise((resolve) =>
        setTimeout(resolve, retryDelayMs(response, attempt)),
      );
      continue;
    }

    if (!response.ok) {
      const body = await response.text();
      throw new Error(`Session verification was rejected (${response.status}): ${body}`);
    }

    return response.json();
  }

  throw new Error("Session verification remained rate-limited after bounded retries");
}

const verifiedSession = await verifySession();
console.log(JSON.stringify(verifiedSession, null, 2));
Enter fullscreen mode Exit fullscreen mode

Run this check before committing the account switch, and validate the returned object against the discovered response schema during integration. The UI should remain attached to the old account while the candidate check is in flight. On success, commit the new session pointer and clear old account data from memory and local caches. On a rejected response, surface the reason without exposing credentials and leave the old account state unchanged.

This is where a self-describing API can remove integration guesswork. Infrai exposes public discovery with the request and response JSON Schema plus runnable examples, while its auth actions use the same plain REST surface under one key; that fits teams that want to read the contract at integration time without adopting another SDK. Its role here is narrow: make the session boundary explicit. It does not replace the product decision about when this tablet should forget the previous account.

Compare the auth options by the boundary you need

A fair shortlist should include the identity experience, session semantics, operational model, and migration cost. Product names alone won't make the decision. The table focuses on the first question to validate in each documented model, because a shared-device flow needs special scrutiny even when ordinary single-user login looks polished.

Option Architectural center Best fit for this decision The catch to validate
Clerk Managed user and session platform with documented multi-session concepts Teams that want account switching represented directly in a managed session model Confirm that its UI behavior, session limits, and device semantics match the exact family-sharing flow
Auth0 Managed identity platform with configurable session and refresh-token controls Organizations that need identity policy controls around an established application More policy surface means more tenant configuration to review and observe
Firebase Authentication Client-SDK-centered authentication with documented auth-state persistence Apps already built around Firebase clients and their state model Check persistence carefully on a physically shared browser or tablet
Supabase Auth Authentication integrated with the Supabase application stack Teams that want auth close to an existing Supabase and Postgres architecture Switching stacks for auth alone may create more migration work than value
Infrai Self-describing REST capabilities under one platform contract Teams that prefer discovery-driven HTTP integration and a small session API boundary It is not the right reason to replace a provider when hosted identity UI or deep tenant policy is the primary requirement

Stick with Clerk when its managed multi-session experience already expresses the switching flow you need. Stick with Auth0 when centralized identity policy is the hard part. Firebase Authentication is the practical choice when client persistence and the rest of the app are already Firebase-shaped; Supabase Auth is similarly coherent inside a Supabase application. A discovery-driven REST option fits when avoiding SDK-specific integration is itself an architectural goal.

The decision rule is compact: choose the smallest set of interfaces that preserves the risk boundaries your app can explain. Phone-code verification proves the phone step. Session creation establishes a new lifetime. Verification gates the UI transition. Refresh maintains continuity under a different risk policy from the short-lived access credential. Revocation ends either one selected session or, only under an explicit stronger action, every session for the user.

No provider should collapse those product meanings for you.

What about friction, refresh, and interrupted switches?

The first objection is friction: why verify a candidate session before showing the selected account? Because rendering cached account data first creates a brief but real cross-account disclosure on a shared screen. Keep the transition screen neutral, do the check, and then hydrate account-specific data. The extra network step is visible, so measure its completion rate and duration in your own environment, but don't trade away isolation by painting the next account early.

Refresh needs a separate risk policy. A short-lived access credential limits the usefulness of an exposed credential, while renewal preserves continuity. The renewal mechanism therefore deserves stronger storage, rotation, and revocation thinking than an ordinary API response. Follow the provider's current session contract and the OWASP guidance rather than inventing a second token protocol inside the app.

The second objection is continuity: should a failed switch sign out the current user? Usually no. A rejected candidate should leave the verified current session intact unless your threat model says the attempt itself is reason to lock the device. The exception must be explicit. For example, a support-admin policy may require a neutral locked screen after suspicious verification activity; a low-risk family media flow may retain the current session and report that the switch did not complete. Your mileage may vary because the risk, not the phone-code mechanism, decides this branch.

The catch is that current-device sign-out and all-device revocation can look nearly identical in a compact UI. Use distinct labels and require a deliberate confirmation for the broader action. Then emit different audit events. If support sees “all sessions revoked,” the record should identify the initiating user, target user, device installation, time, and correlation identifier without retaining secrets.

A final test matrix should cover two accounts on one device, the same account on two devices, a switch abandoned before code verification, a candidate session rejected before commit, a refresh during an active account, current-device sign-out, and all-device revocation. Test the visible data as well as the token state. The strongest implementation is the one where every row has an unambiguous active account and an audit trail that explains why.

References

Further reading

Top comments (0)