Multi-Identity Account Page Listing and Safely Removing Login Methods Explained
Short answer: treat login methods as recoverable credentials, not disposable profile fields. A multi-identity account page should only remove a method after proving the user still has a recovery path, step-up authenticating for the action, and revoking every session that could keep the old identity alive. In a fintech product, that sequence matters more than a polished unlink button: an accidental lockout becomes a support event, while a stale session can become a privacy incident. The decision table I use before touching an identity Start with a server-side inventory. The page may show Google, passkey, email, phone, or an enterprise subject, but the API owns the facts: when each method was verified, when it was last used, and whether it is the final recovery route. Account state Remove action Required recovery check Session action Two verified methods, one recent Allow after step-up authentication Confirm the other method works now Revoke sessions minted through the removed method One method plus an unverified address Block removal Ask the user to verify a second method first Keep sessions, record the blocked attempt Passwordless passkey only Offer replacement enrollment first Complete a fresh passkey or recovery-code ceremony Revoke all sessions after replacement, then remove Regulated account under review Defer to account-recovery workflow Human review or documented identity proofing Revoke suspicious sessions immediately if policy says so Pick the first row when the person can demonstrate an alternate method in the same session. Pick the second when the page is being used to delete the only viable route; “unverified” is not a recovery plan. The third is common after a device replacement. The fourth belongs to your risk and compliance policy, not a generic UI rule. I keep this table in the design review because teams otherwise optimize for a single happy path. The dangerous state is not “the delete request failed.” It is “the request succeeded, but nobody can explain how the account can be recovered tomorrow.” How should a multi-identity account page safely remove login methods? The page is a read model, not an authority. On GET /account/identities, return stable identifiers, display labels, verification state, lastUsedAt, and a boolean such as canRemove. Never return provider access tokens or raw subject identifiers. Render the remove control from those facts, then re-check them on the server because the account can change between page load and click. The removal endpoint should be deliberately boring: type RemoveIdentity = { identityId: string; reason: "user_requested"; idempotencyKey: string; }; async function removeIdentity(input: RemoveIdentity, actor: Actor) { await requireRecentStepUp(actor, { maxAgeSeconds: 300 }); return db.transaction(async (tx) => { const identity = await tx.identities.lockForUpdate(input.identityId); if (!identity || identity.accountId !== actor.accountId) { throw new Error("identity_not_found"); } const remaining = await tx.identities.countVerified(actor.accountId, { excluding: identity.id, }); if (remaining === 0) throw new Error("recovery_path_required"); await tx.identities.markRemoved(identity.id, { reason: input.reason }); await tx.sessions.revokeByIdentity(identity.id); await tx.audit.append({ type: "identity_removed", accountId: actor.accountId, identityId: identity.id, idempotencyKey: input.idempotencyKey, }); }); } The transaction lock closes a race where two browser tabs remove the last two methods at once. Imagine the concrete sequence: tab A reads “email plus passkey,” tab B reads the same snapshot, A removes email, and B removes passkey milliseconds later. Without a row lock and a fresh count inside one transaction, both requests pass a client-side check and the account becomes unreachable. With the lock, one transaction commits first; the second sees zero remaining verified methods and returns recovery_path_required. The idempotency key makes a retry safe; a mobile client should be able to repeat a timed-out request without creating a second audit event, even after the first response was lost on a flaky network. Return a generic success shape for an already-removed identity, but keep authorization checks strict so an attacker cannot use that response to probe another account. Pause. Step-up authentication must be bound to the account and the action. A login from five minutes ago is evidence of a session, not evidence that the person intends to delete a credential now. WebAuthn, a fresh password check, or a recovery-code ceremony can satisfy the step-up policy; SMS may be a fallback with a different risk rating. OWASP recommends reauthentication for high-risk account changes and careful handling of recovery factors, so document your factor policy instead of hiding it in frontend code. What fails when identity removal is treated as a CRUD operation? The first failure mode is orphaning. A user removes an old email while a passkey enrollment is still pending, and the system counts that pending record as a verified fallback. The next login challenge has nowhere valid to go. Count only active, verified methods that have passed their own verification ceremony. The second is session confusion. Deleting a row in identities does not invalidate refresh tokens, remembered devices, or sessions issued by another service. Model revocation as an event consumed by every session authority, and include a token version or revocation timestamp in access checks. Expect a small propagation window; your threat model should state how long it is and what high-risk actions require a fresh session. The third is observability blindness. A 200 response tells you the database changed, not that all sessions stopped. Emit structured events for the request, policy decision, identity mutation, and revocation result. Alert on repeated recovery_path_required decisions, removal attempts from new devices, and a spike in revocation lag. I want the account ID, identity ID, actor session ID, correlation ID, and policy version in the audit record. I don't want secrets. That invariant matters. A small test matrix beats a large account page Test the state transitions as contracts. Property-based tests are useful for the invariant: after a successful removal, at least one active verified recovery method remains. Add concurrency tests for two simultaneous removals, replay tests for the same idempotency key, and authorization tests that swap identity IDs between accounts. For browser coverage, exercise back-button replay and two tabs. Verify that an old refresh token is rejected after revocation, while an unrelated session follows the policy you chose. Test clock skew around the five-minute step-up window. One second on either side can expose a production-only bug. Keep the UI honest. Disable the control while the request is pending, show which recovery method remains, and provide a path to enroll a replacement before destructive action. A confirmation dialog is not a security control; it is merely a chance to explain the consequence in plain language. Limits and choosing a different recovery path This pattern is not suitable when your organization cannot verify a second factor or cannot propagate revocations across services. In that case, stick with a dedicated, human-reviewed account-recovery process and make the page submit a request rather than silently unlinking anything. High-value fintech accounts may also need transaction holds or support review after identity changes. I’m not sure a single canRemove boolean will stay sufficient as risk policy evolves; your mileage may vary. Version the policy response, expose a reason code for support tooling, and revisit the table whenever you add a new identity provider. The durable rule is simple: removal is allowed only when recovery remains demonstrably possible, the actor just reauthenticated, and every session tied to the removed method has a known revocation outcome. References https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html https://www.w3.org/TR/webauthn-3/ https://datatracker.ietf.org/doc/html/rfc7009 https://datatracker.ietf.org/doc/html/rfc6749
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to