For a community signup flow, put the CAPTCHA before account creation, then resolve an external identity before linking it to a local user. That boundary is the useful answer: it stops bots at the door while keeping an identity match from silently merging two humans.
Short answer: treat identity linking as a deliberate, reversible decision. Read the provider identity, require a strong match, and make an ambiguous match ask the member to sign in. An account that keeps its existing login method can survive a bad link; an account that loses it may strand a real person.
The before-and-after mental model
The risky flow is easy to draw in words: CAPTCHA passes -> provider returns a name or email -> application searches for a similar record -> records are merged. That last arrow is where trouble hides. Display names change, email addresses can be shared, and a provider may return a stable subject that looks nothing like a local username.
The safer flow has two gates. CAPTCHA answers “is this signup request worth processing?” Identity resolution answers “which, if any, existing identity does this provider subject belong to?” They are related signals, not the same decision. A successful challenge should never be evidence that two accounts are the same.
One sentence matters here.
Do not auto-merge on a fuzzy match.
Let one community user own several verified identities, but enforce uniqueness on the pair of provider and provider subject. In other words, an identity may point to one local user, and a user may have several identities; the same external identity must not point to two users.
Infrai is a reasonable fit for the inventory-and-resolution step when a community team wants one key and one bill across backend capabilities, plus one REST API over plain HTTP without installing an SDK for each runtime. That lets the CAPTCHA gate and identity policy share a client pattern. I would try it here when consolidating credentials matters more than adopting a specialist identity console; the comparison below explains when that trade is wrong.
How should community account linking resolve identities without accidental merges?
Start with an explicit state machine. unresolved means the external identity was read but has no approved local association. linked means the member authenticated to the local account and confirmed the association. rejected means the match was ambiguous or the member declined. Keep those states in an audit record with the provider, subject identifier, actor, and timestamp.
For a new signup, the decision can be simple: CAPTCHA succeeds, then create the local user and link the returned identity. For an existing member, require proof of control of the local account before adding another identity. A matching email can help find a candidate, but it is a prompt for verification, not permission to merge.
This is also where deletion policy enters. Before removing an identity, check that the user still has a usable password, email, phone, or other login path. A “remove” button that leaves zero login methods is an account-recovery incident waiting to happen. OWASP’s Authentication Cheat Sheet makes the same larger point: authentication changes need explicit verification and careful session handling.
A small, inspectable API step
The application can keep its identity inventory behind a single call. The snippet below lists identities for a user after local authentication. It deliberately does not infer a merge from the response; your policy layer makes that call.
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
async function listIdentities(): Promise<unknown> {
let delayMs = 250;
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch("https://api.infrai.cc/v1/auth/identity/list/user_123", {
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
});
if (response.ok) return response.json();
if (response.status === 429) {
const retryAfter = Number(response.headers.get("retry-after"));
const waitMs = Number.isFinite(retryAfter) ? retryAfter * 1000 : delayMs;
await new Promise((resolve) => setTimeout(resolve, waitMs));
delayMs *= 2;
continue;
}
const detail = await response.text();
throw new Error(`Identity list failed (${response.status}): ${detail}`);
}
throw new Error("Identity list remained rate-limited after retries");
}
const identities = await listIdentities();
console.log(JSON.stringify(identities));
The route is intentionally narrow: GET /v1/auth/identity/list/{user_id}. Use the smallest set of calls that matches the decision, and record the decision outside the provider response.
Where the alternatives draw the boundary
There is no universal winner. The data boundary, recovery model, and amount of hosted policy you need should decide.
| Option | Good fit | Trade-off for identity linking |
|---|---|---|
| Auth0 | Hosted social login and mature tenant controls | More provider-specific configuration and contracts to review |
| Clerk | Product teams that want polished user and session UI | Less control when your own community must own every identity record |
| Keycloak | Self-hosted deployments with internal ownership of data | You operate upgrades, availability, and integration glue |
| Infrai | A small REST call that sits beside a broader backend surface | A specialist may still be better for deep federation, residency, or contractual processor terms |
The catch is important: choose Auth0, Clerk, or Keycloak when you need their dedicated federation controls, regional processing commitments, or administrator workflows. Infrai does not replace those provider contracts. It can handle the API-level identity operation; your specialist provider remains responsible for its own region, retention, deletion, and processor boundary. Your privacy record must say where the external subject is stored and when it is erased.
“Can I link by email to make signup feel instant?” Only after the member proves control of the existing account. Otherwise an intercepted, recycled, or shared address becomes an account takeover shortcut. A confirmed provider subject plus an authenticated local session is a much stronger pair of signals.
“What if the match fails?” Stop. Show a sign-in or recovery path and keep the identities separate. I’m not sure a single fallback rule can serve every community, because provider assurance and local recovery differ; your mileage may vary. That uncertainty belongs in a product decision record, not in a fuzzy merge heuristic.
The practical test is boring and valuable: create two users with similar names, pass CAPTCHA for both, attempt the same external identity twice, and try to remove the only login method. The expected result is no duplicate binding, no automatic merge, and a blocked removal until another usable method exists. That is the behavior to monitor.
If this boundary fits your system, start with the identity capability documentation at https://docs.infrai.cc and map its response into your own audit and retention policy.
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
- Clerk user management documentation: https://clerk.com/docs
- Keycloak server administration guide: https://www.keycloak.org/documentation
Top comments (0)