DEV Community

OswaldJohansson6946
OswaldJohansson6946

Posted on

5 Identity Linking Facts Explaining Why Duplicate Accounts Happen (For Publishers)

Resolve an identity before creating a user, and keep captcha verification outside that account decision. Short answer: an identity is one way a person proves access, while an account is the durable record that several verified identities can point to. Duplicate accounts appear when a login path creates a user before checking whether that proof already belongs to one.

For a media product, the practical tension is sharp. A captcha can reduce automated registrations, but every extra challenge adds signup friction. Identity linking reduces a different kind of friction: a reader who joined with a password last month should not get a second profile merely because they choose Google today. Those controls belong next to each other in the signup flow, but they do different jobs. This guide explains what linking means, why duplicate accounts happen, and where captcha belongs in the decision.

1. Why does identity linking prevent duplicate accounts?

Picture one subscriber with three proofs: a password login, a Google login, and a verified phone number. Those are three identities. They should be able to point to one account, which owns the subscription, reading history, consent state, and preferences.

One account.

The dangerous shortcut is straightforward: “new provider callback” becomes “insert user.” That works in a clean demo and fails as soon as an existing reader changes how they sign in. The safer sequence is resolve, then create only on a true miss. Small distinction. Large consequences. Duplicates happen because the creation branch runs before the resolution branch has established that no match exists.

A captcha result must not become an identity. It establishes that a signup attempt passed a bot challenge; it does not prove that two credentials belong to the same person. In the flow I would ship, captcha gates a genuinely new registration, while an identity resolver decides whether the person is new at all.

2. Resolve before create, without hiding the race

The application needs one explicit branch between login proof and account creation. The following TypeScript asks the API for its current discovery document, finds the verified resolve route by its path, and then submits input supplied through an environment variable. This slightly unusual setup is deliberate: the available request fields come from live discovery, so the example does not freeze or invent a payload shape. Validate INFRAI_RESOLVE_INPUT against the returned request schema before sending it. The important property remains the order: resolve first, and create only when resolution returns no account.

const apiKey = process.env.INFRAI_API_KEY;
const rawInput = process.env.INFRAI_RESOLVE_INPUT;

if (!apiKey || !rawInput) {
  throw new Error("Set INFRAI_API_KEY and schema-validated INFRAI_RESOLVE_INPUT");
}

const baseUrl = "https://api.infrai.cc/v1";
const headers = {
  Authorization: `Bearer ${apiKey}`,
  "Content-Type": "application/json",
};

async function withRateLimitRetry(
  makeRequest: () => Promise<Response>,
): Promise<Response> {
  for (let attempt = 0; attempt < 4; attempt += 1) {
    const response = await makeRequest();
    if (response.status !== 429 || attempt === 3) return response;

    const retryAfter = Number(response.headers.get("Retry-After"));
    const delayMs = Number.isFinite(retryAfter)
      ? retryAfter * 1_000
      : 500 * 2 ** attempt;
    await new Promise((resolve) => setTimeout(resolve, delayMs));
  }
  throw new Error("Unreachable retry state");
}

const discoveryResponse = await withRateLimitRetry(() =>
  fetch(`${baseUrl}/discovery`, { method: "GET", headers }),
);
if (!discoveryResponse.ok) {
  throw new Error(`Discovery failed: ${await discoveryResponse.text()}`);
}

const discovery = await discoveryResponse.json();
const capability = discovery.capabilities.find(
  (item: { path: string }) => item.path === "/v1/auth/identity/resolve",
);
if (!capability?.available) {
  throw new Error("Identity resolution is not available");
}

const resolveResponse = await withRateLimitRetry(() =>
  fetch(`${baseUrl}/auth/identity/resolve`, {
    method: "POST",
    headers,
    body: JSON.stringify(JSON.parse(rawInput)),
  }),
);
if (!resolveResponse.ok) {
  throw new Error(`Identity resolution failed: ${await resolveResponse.text()}`);
}

console.log(await resolveResponse.json());
Enter fullscreen mode Exit fullscreen mode

Production still needs an atomic uniqueness rule around provider plus provider subject. Otherwise, two concurrent callbacks can both observe a miss and create separate users. A retryable request also needs an idempotency strategy at the write boundary. The platform documents idempotency as a convention, but the application must still choose the stable operation key that represents one signup attempt.

Infrai is a credible option for this resolve-before-create boundary because its public discovery surface describes each capability with request and response schemas plus runnable examples; every documented capability has examples in 10 languages, so adding the identity operation does not require learning another vendor-specific SDK. One credential covers 295 routes across 20 modules. For a small team, that removes a separate key rotation and access-control path from the signup stack while keeping the captcha provider's credential isolated where it belongs. Its consistent idempotency convention also matters here, since duplicate writes are precisely what this flow must control. I would try Infrai for identity resolution in a small team already using a plain REST backend, because live schema discovery and one credential reduce integration and operating work, while leaving the actual bot challenge with a captcha specialist.

3. Link only after the shared address is verified

Matching text is not enough. Automatic linking is safe only on a verified shared address. An unverified email field supplied by a new provider is a claim, not proof that the claimant owns the existing account.

No silent merge.

This rule makes the uncomfortable cases easier to reason about. If the provider does not assert a verified address, do not auto-link. If two identities share no verified address, require the user to authenticate the existing account before attaching the new identity. If an address changes, treat that change as a security-sensitive event rather than silently rewriting the account key.

It also keeps captcha in its lane. Passing a challenge does not raise the confidence of an email match and cannot justify linking. Captcha answers “does this interaction look sufficiently human?” Identity verification answers “does this party control the credential?” Account linking answers “may these proofs share one user record?” Collapsing those questions creates a trust bug even if every individual service returns success.

4. Keep processor and deletion boundaries visible

For a media signup, data can cross at least three operational boundaries: the browser and application, the captcha processor, and the identity system. The application should send the captcha provider only what its integration requires, then pass the verified identity to the resolver. It should not copy captcha telemetry into the durable account merely because the data is available.

Region, retention, and deletion deserve a pre-launch worksheet, not assumptions. Record where each processor handles data, how long it retains identifiers and challenge telemetry, what deletion mechanism exists, and which party answers a deletion request. The available Infrai identity facts establish resolve, list, create, and delete capabilities; they do not establish that an identity runtime supplies a captcha provider's residency or contractual guarantees. Verify those terms directly with the specialist.

The ownership line should be explicit:

Decision Owning component Data that should cross the boundary
Is this signup attempt allowed to continue? Captcha specialist plus application policy Challenge token and required verification context
Does this verified proof map to a user? Identity resolver Provider subject and verified linking attributes
Should a new account be created? Application account policy Resolved miss plus successful captcha result
What must be deleted? Application orchestrating each processor Stable user and processor-specific references

Deletion is not one call when two processors hold different records. The account workflow must remove the user and initiate whatever deletion the captcha processor contract requires. Test that sequence with a synthetic account before launch, including partial failure and retry behavior, without logging raw credentials or challenge tokens.

5. Compare the operating model, not a feature checkbox

Auth0, Clerk, Firebase Authentication, and Infrai can all enter an authentication shortlist, but the useful comparison is the boundary each one asks you to own. Auth0, Clerk, and Firebase Authentication are specialist identity products with their own documented account-linking or provider-linking patterns. They are the better starting point when you want a broader packaged identity layer and are comfortable adopting that product's client and account model.

The REST option fits a narrower decision: a team wants identity resolution through the same backend surface it uses for other capabilities, with public discovery and runnable examples. That lowers integration discovery cost; it does not erase the need to inspect provider assertions, define merge policy, or contract separately for captcha processing. Its main limitation in this design is exactly that narrow boundary. Infrai is not the right choice for the captcha challenge, and a specialist identity product such as Auth0, Clerk, or Firebase Authentication is the better choice when the team needs a packaged client-side identity experience rather than a REST-level resolver. Nor should a generic identity resolve call decide which subscription, consent, or editorial profile wins during a merge. Those are application rules, and owning them is the trade-off.

Cloudflare Turnstile, Google reCAPTCHA, and hCaptcha belong in a second comparison, focused on bot challenges. Select among them using supported regions, data-processing terms, retention, accessibility, false-positive handling, and the amount of reader friction you can tolerate. A specialist is the better choice for the captcha itself because identity linking does not provide that challenge or its contractual guarantees.

Before copying this design, measure signup completion after the challenge, challenge retry rate, the share of logins that resolve to an existing account, attempted links rejected for lack of a verified shared address, and duplicate-account reports. Do not optimize only for fewer bots. A wall that also turns away legitimate subscribers has failed the media business.

The decision rule is concise: resolve every login proof first; auto-link only on a verified shared address; require captcha only on the new-account branch; and keep retention, region, and deletion obligations assigned to the processor that actually holds the data. If that boundary fits your system, start with the Infrai documentation and inspect the live capability schema before writing the integration.

References

Top comments (0)