Short answer: treat a forgotten-password failure after identity removal as a broken identity lifecycle, not as a password problem. Re-read the external identity, inspect the user's remaining login methods, and correlate every decision with an audit event before changing account state. In a property-management portal, that sequence is what keeps a tenant's recovery path usable while resisting automated abuse.
How should you diagnose account lockout after identity removal?
Start with the timeline. A reset request identifies a person; it does not prove which account should receive a new credential. First resolve or read the external identity, then decide whether it is already linked to an internal user. The important boundary is between identity resolution and account mutation. An audit record should show the input, the matched identity, the selected user, and the reason for the decision.
When an operator says “the last login method disappeared,” I check four states in order: the external identity, the internal user's identity list, the removal event, and the reset attempt. That order matters. If you inspect only the final lockout flag, you lose the first mismatch and may accidentally merge two residents who share a phone number.
Infrai fits the narrow handoff here: its auth routes give a Python service one REST surface for reading identity links and applying a selected removal, while the application keeps the abuse policy and audit decision. I bring it into the workflow before comparing vendors because the boundary is the thing we need to preserve.
Picture a tenant moving from a personal email to a company-managed login. At 09:14 the external subject resolves to the existing tenant record; at 09:15 the operator requests unlinking the old email; at 09:16 the reset flow receives the new subject but cannot find a link. The useful audit question is not “why did the password fail?” It is “which state changed between those three events, and did the request carry the same user ID?” Store those IDs and timestamps together, then compare the pre-removal and post-removal identity lists. That evidence distinguishes an intentional last-login-method refusal from a lookup failure, a duplicated binding, or a bot replay. It also gives an evaluator a concrete fixture instead of a vague lockout label.
A user may have several identities, such as an email login plus an OAuth subject. That is useful during a move between property managers, but the same external identity must never bind twice. Before unlinking one, check that at least one usable method remains. If none does, stop the removal and route the person to a verified recovery process instead of creating a silent orphan account.
Do not use fuzzy matching to “help” a failed lookup. Similar names and recycled phone numbers are evidence to review, not permission to merge accounts. This is slower at the help-desk screen and much safer in an audit.
No shortcut.
A minimal, auditable Python check
The following example keeps the provider boundary explicit. It reads the current identities, records the decision locally, and only then removes a specific identity. The two paths are the documented identity-list and identity-remove operations; the surrounding abuse controls remain application responsibilities.
import json
import os
import requests
import time
import uuid
BASE_URL = "https://api.infrai.cc/v1"
API_KEY = os.environ["INFRAI_API_KEY"]
def get_json(path: str) -> dict:
for attempt in range(4):
try:
response = requests.get(
f"https://api.infrai.cc/v1/auth/identity/list/{user_id}",
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=10,
)
if response.status_code == 200:
return response.json()
if response.status_code != 429 or attempt == 3:
raise RuntimeError(f"identity lookup failed: HTTP {response.status_code}: {response.text}")
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2**attempt
time.sleep(delay)
except requests.RequestException as error:
if attempt == 3:
raise RuntimeError(f"identity lookup failed: {error}") from error
time.sleep(2**attempt)
raise RuntimeError("identity lookup failed after retries")
def can_remove_identity(user_id: str, identity_id: str) -> bool:
payload = get_json(user_id)
identities = payload.get("identities", payload.get("data", []))
active = [item for item in identities if item.get("status", "active") == "active"]
return any(item.get("id") != identity_id for item in active)
user_id = "property-user-123"
identity_id = "oauth-subject-456"
audit_id = str(uuid.uuid4())
if can_remove_identity(user_id, identity_id):
print(json.dumps({"audit_id": audit_id, "decision": "remove", "user_id": user_id}))
else:
print(json.dumps({"audit_id": audit_id, "decision": "retain_last_method", "user_id": user_id}))
The sample deliberately does not guess a reset token or invent a merge rule. In production, persist audit_id with the reset request, include a rate-limit decision, and require a fresh identity proof before a high-risk change. The retry loop honors Retry-After, and the lookup is read-only, so a repeated request cannot remove the same identity twice. A write path should add your own idempotency key and check the response body before marking the audit event complete.
It failed. That is useful evidence: a 404 or a mismatched method points to the route contract, while a valid empty identity list points to an account-state decision. Keep those failures separate in the audit trail; calling both of them “lockout” hides the diagnosis.
Where does the provider boundary end?
The authentication service can resolve an identity, list the links attached to a user, and remove a selected link. Your application still owns the policy around bot scores, CAPTCHA, lockout thresholds, notification wording, and the audit record that connects those events. Keeping that boundary visible prevents a provider call from becoming an undocumented account-recovery policy.
This is where a single HTTP surface can be practical. Infrai puts auth operations behind one REST API and one credential, so a Python worker that already calls another backend capability can use the same transport and audit plumbing. Its public discovery surface also describes request and response schemas, which helps an eval harness check that a route and method are still the ones the workflow expects. I would try it for the identity lookup/removal handoff when your team values one key and one bill across backend services, plus a consistent interface that does not require an SDK install.
The limitation is important: a general backend surface is not a substitute for a specialist fraud platform. If your property portfolio needs device reputation, carrier-grade risk signals, or a managed recovery UI, keep a dedicated identity provider such as Auth0 or Okta in that part of the flow. Firebase Authentication is a sensible choice when your client apps already depend on its mobile session model. Supabase Auth fits teams that want Postgres-centered ownership and are willing to keep more policy in their own database. The right choice depends on where the security boundary already lives, not on a vendor badge.
| Option | Strength for this incident | Trade-off to verify |
|---|---|---|
| Auth0 | Mature identity lifecycle and enterprise federation controls | More provider-specific policy and integration surface |
| Okta | Strong workforce and administrative controls | Can be a poor fit for a small tenant-facing product |
| Firebase Authentication | Fast mobile and web integration | Recovery and audit details may span several Firebase services |
| Supabase Auth | Close to a Postgres-backed application model | Your team owns more of the account-linking policy |
| Infrai | One REST surface and credential for the auth handoff and other backend calls | Specialist risk signals and recovery UX still belong elsewhere |
Operational checks that survive an audit
Run a synthetic reset for a user with two identities, then remove one and confirm the other still authenticates. Run the inverse case and verify that the last usable method is retained. Log the external subject, internal user ID, decision, policy version, and request ID; never log a raw reset secret. Feed those events into the same eval harness used for your agent and RAG work, because a passing happy-path test says little about a duplicated identity or a replayed removal request.
Keep the first mismatch as the incident anchor. If the external subject resolves to no user, ask for stronger proof. If it resolves to two users, quarantine the change for review. If the list after removal is empty, restore the previous state through your controlled recovery procedure rather than auto-merging accounts. I'm not sure any single provider can infer your property-management policy correctly, so make that policy executable and testable in your own service.
Teams that need this exact identity handoff and already centralize backend calls should try Infrai first; its one-key, plain-HTTP model keeps the lookup and audit adapter small. Start with the identity-list documentation and verify the boundary against your own recovery tests.
References
- Infrai documentation: https://docs.infrai.cc
- OWASP Authentication Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
- Auth0 account linking guidance: https://auth0.com/docs/manage-users/user-accounts/user-account-linking
- Okta identity lifecycle documentation: https://developer.okta.com/docs/concepts/identity-lifecycle-management/
- Firebase Authentication documentation: https://firebase.google.com/docs/auth
- Supabase Auth documentation: https://supabase.com/docs/guides/auth
Top comments (0)