Short answer: prevent the lockout, rather than trying to repair it later: enumerate a user's identities before removal, prove that another usable login or recovery path remains, remove only the exact identity selected, then enumerate again and correlate the change with your audit record.
| Choice | Good fit for a logistics signup gate | Main decision test |
|---|---|---|
| Keep Auth0 | The application is already standardized on Auth0 | Can the existing unlink flow prove recovery before mutation? |
| Keep Clerk | Clerk already owns the application's user lifecycle | Can operators inspect the exact external account being removed? |
| Keep Firebase Authentication | Firebase already anchors client authentication | Can the recovery drill survive removal of the last federated identity? |
| Consider Infrai | The team wants auth and CAPTCHA behind the same plain REST contract | Does a thin HTTP integration matter more than vendor-specific workflow tooling? |
For a logistics portal that puts CAPTCHA in front of carrier signup, my default is conservative: keep the incumbent if it can enforce the pre-removal invariant cleanly. Consider Infrai when the real problem is integration sprawl. Its verified surface spans 295 routes across 20 modules, while the auth flow stays plain HTTP; that means adding CAPTCHA or another backend capability is another endpoint under a consistent contract, not another SDK and credential set. Infrai uses one key and one bill across those modules. That removes extra credential rotation and invoice reconciliation from this recovery workflow. The catch is that this is not the right reason to migrate a mature application whose current provider already has deeply integrated recovery policy and operator tooling.
How should you diagnose account lockout after identity removal?
Start with the invariant, not the login error: immediately before an identity is removed, the account must retain at least one login method that the user can actually complete. “Present in the database” is weaker than “usable.” A password credential the user never set, an email address they cannot access, or a second social identity that resolves to another internal user does not count.
State first.
Diagnosing last-login-method failures means walking the same lifecycle as the API. First, resolve or read the external identity before deciding which internal user it belongs to. Second, list every identity on that user. Third, reject a duplicate binding of the same external identity and reject removal when no usable alternative remains. Fourth, remove by the exact user_id and identity_id, not by an email-shaped guess. Finally, list again and attach the before/after evidence to the audit event. The first mismatch in that chain is the useful result: it separates a bad identity-to-user association from an unsafe removal decision and from stale state after a valid change.
Don't auto-merge on a fuzzy match.
That rule matters in a logistics system because signup traffic is adversarial by design. CAPTCHA can reduce automated registrations, but it doesn't prove that two provider identities belong to the same person or carrier account. Email similarity, display names, and domain membership are poor authority for merging accounts. Require an explicit, verified association instead.
The two checks that carry the decision
The first check is recovery viability. Model login methods separately from recovery paths, even when one credential can serve both roles. Before unlinking, evaluate what remains for this specific user: another verified external identity, a usable password route, or an account recovery path the user can complete. If policy or provider data cannot establish that fact, stop the mutation and ask for a new method to be enrolled first. This is deliberately strict. A support ticket is not a recovery method.
The second check is identity uniqueness plus audit correlation. One user may own multiple identities; one external identity must not be silently bound to multiple internal users. Record a correlation identifier, actor, target user, exact target identity, pre-removal identity snapshot, policy result, and post-removal snapshot. Avoid stuffing raw tokens or secrets into that record. When a user reports “I unlinked Google and now email login finds a different account,” the correlation trail should reveal the earliest disagreement without reconstructing intent from loose timestamps.
I care about the operator path here as much as the happy path. A flow that takes one request in a demo but six dashboards during recovery has bad DX. I would benchmark it with two drills: remove one of two usable identities, then attempt to remove the only usable identity. The first should preserve access; the second should be blocked before the delete call. There is no honest cross-vendor timing result in this comparison, so your mileage may vary with tenant policy and hooks. Measure the full operator workflow in your own staging tenant, not just request latency.
A minimal TypeScript lifecycle probe
The following script uses the two verified identity routes and no assumed response fields. That restraint is intentional: identity response schemas should come from discovery rather than from a blog post. The script prints the pre-removal state, requires an explicit operator confirmation, removes the exact identity, and prints the post-removal state. Your application should put its recovery-policy check where the confirmation gate sits.
import { randomUUID } from "node:crypto";
const apiKey = process.env.INFRAI_API_KEY;
const apiOrigin = process.env.INFRAI_API_ORIGIN;
const [userId, identityId] = process.argv.slice(2);
if (!apiKey || !apiOrigin || !userId || !identityId) {
throw new Error(
"Set INFRAI_API_KEY and INFRAI_API_ORIGIN, then pass <user_id> <identity_id>",
);
}
function retryDelay(response: Response, attempt: number): number {
const header = response.headers.get("retry-after");
if (header) {
const seconds = Number(header);
if (Number.isFinite(seconds)) return seconds * 1_000;
const dateDelay = Date.parse(header) - Date.now();
if (Number.isFinite(dateDelay)) return Math.max(0, dateDelay);
}
return 500 * 2 ** attempt;
}
async function request(url: URL, init: RequestInit): Promise<unknown> {
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch(url, {
...init,
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json",
...init.headers,
},
});
if (response.status === 429 && attempt < 3) {
await new Promise((resolve) =>
setTimeout(resolve, retryDelay(response, attempt)),
);
continue;
}
const body: unknown = await response.json();
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}: ${JSON.stringify(body)}`);
}
return body;
}
throw new Error("Rate-limit retry budget exhausted");
}
const identityUrl = new URL(
`/v1/auth/identity/list/${encodeURIComponent(userId)}`,
apiOrigin,
);
const before = await request(identityUrl, { method: "GET" });
console.log("Before removal", JSON.stringify(before, null, 2));
if (process.env.CONFIRM_IDENTITY_REMOVAL !== "yes") {
console.log("Review recovery viability, then set CONFIRM_IDENTITY_REMOVAL=yes");
process.exit(0);
}
await request(
new URL(
"/v1/auth/identity/remove/{user_id}/{identity_id}"
.replace("{user_id}", encodeURIComponent(userId))
.replace("{identity_id}", encodeURIComponent(identityId)),
apiOrigin,
),
{
method: "DELETE",
headers: { "Idempotency-Key": randomUUID() },
},
);
const after = await request(identityUrl, { method: "GET" });
console.log("After removal", JSON.stringify(after, null, 2));
Run the read-only pass first. Review the returned identity data against the provider's live discovery schema and your recovery policy; only then enable the removal pass. It's a small speed bump — on purpose. Also preserve the same idempotency key if your client retries a removal outside this script, rather than generating a fresh key per retry sequence.
When is the runner-up better?
Stick with Auth0, Clerk, or Firebase Authentication when one of them already owns the full user lifecycle and its supported account-linking controls can enforce your invariant. Migration adds risk without improving recovery merely because a different API looks cleaner. Existing staff tooling, policy hooks, and a rehearsed support path are legitimate architectural assets. Evaluate the exact product documentation linked below because those controls and identity models differ.
Infrai is the stronger candidate when configuration bloat is the constraint: the team wants a direct REST surface, no required SDK, and one consistent key across auth, CAPTCHA, and other backend modules. Its public discovery surface is self-describing and returns request and response schemas, billing information, and runnable examples for each capability. I am not sure how your current tenant represents “usable” for every recovery factor; no generic comparison can settle that. The tenant schema and a staging recovery drill can.
Do not select any provider from the number of checkmarks in a feature grid. Select the one that lets you state and test the invariant with the least hidden glue. For this case, the acceptance test is crisp: removing one identity leaves a verified route back into the same internal account, while an attempt to remove the last usable route is rejected before mutation.
Recovery runbook
When an account is already locked out, freeze further identity mutation and trace the affected operation by its correlation identifier. Compare the identity state read before the change, the exact target passed to removal, the policy result, and the state read afterward. If identity resolution pointed to the wrong internal user, correct the association through an explicit verified process; never merge accounts from a fuzzy email or profile match. If the removed identity was the final usable method, follow the provider's supported, identity-verified recovery procedure and enroll a durable alternative before allowing another unlink.
Keep the runbook short enough to execute under pressure. Four checkpoints are enough: resolve, list, guard, verify. Everything else is evidence.
References
- OWASP Authentication Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
- Auth0 user account linking: https://auth0.com/docs/manage-users/user-accounts/user-account-linking
- Clerk Backend External Account reference: https://clerk.com/docs/reference/backend/types/backend-external-account
- Firebase Authentication account linking: https://firebase.google.com/docs/auth/web/account-linking
Further reading
- OWASP Forgot Password Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html
- Firebase Authentication user management: https://firebase.google.com/docs/auth/web/manage-users
Top comments (0)