For a marketplace, the least complex safe choice is proof-based linking: require control of the existing account, validate the new OpenID Connect login on the server, and then attach its issuer-and-subject pair in one transaction. Do not merge users merely because Google returns the same email address.
TL;DR: choose authenticated linking for an existing password account. Auto-merge by email removes one interaction, but it turns an email attribute into an account-ownership credential. Keep a compact, access-controlled link audit record for a declared period; this article uses 30 days as a planning policy, not a universal standard. Retain aggregate counters longer, without user identifiers.
Start with the bill because it exposes the design error early. In an illustrative marketplace handling 2,000,000 authentication attempts per day, logging a 1.2 KB structured event for every attempt produces about 2.4 GB/day before indexing overhead and replicas. At 30 days, that is 72 GB of raw event bodies. If only 0.4% of attempts reach the account-linking boundary, full-fidelity link evidence is about 9.6 MB/day under the same event-size assumption. The dominant term is ordinary sign-in telemetry, not the rare link decision.
That arithmetic changes the design: sample routine successes, count them with low-cardinality dimensions, and preserve complete records only for security-sensitive transitions. The result is less stored data and a clearer investigation trail.
How should a login link to an existing email account?
An OpenID Connect identity is identified by the combination of iss and sub. The specification states that the subject identifier is locally unique and never reassigned within the issuer, and that the client must validate the issuer and audience of an ID token. Email is a claim about an address. Even when email_verified is true, it is not a durable foreign key for the marketplace's user table.
The distinction matters at the exact moment a returning seller selects “Continue with Google.” Suppose the marketplace already has a password account for merchant@example.com, but no external identity row. The server may use the email match to discover a possible linking path. It must not silently decide that both principals are the same person.
Email auto-merge and proof-based linking therefore have different failure surfaces. For an API approach meant to avoid duplicate users, this is the decision point that matters:
| Decision | User friction | Security boundary | Operational consequence |
|---|---|---|---|
| Merge on matching email | Low | Trusts an attribute as ownership proof | Hard to distinguish a legitimate link from an incorrect merge |
| Link after existing-account authentication | One additional proof | Requires control of both sessions | Produces an explicit, reviewable state transition |
Choose the second row whenever an account already exists. For a new signup with no matching account, create the user and external identity together, subject to the marketplace's normal abuse controls. The boundary is crisp: email can locate a candidate account; it cannot authorize mutation of that account.
Proof has a price.
The limitation of proof-based linking is recovery friction: a legitimate user who has lost the password-account session cannot link immediately. They must recover that account through a separately protected flow, and support cannot bypass the ownership check merely because two email strings match. This approach is also unnecessary inside a closed enterprise identity migration where one authoritative administrator has already established both identity records, the merge is reversible, and every mutation is audited. In that narrower setting, a controlled batch reconciliation is the better tool.
Make linking a state transition, not a callback side effect
The callback should first validate the authorization response according to OpenID Connect and OAuth guidance: exact redirect URI handling, state correlation, issuer validation, audience validation, signature validation, nonce validation where applicable, and time-claim checks. The authorization code must be redeemed by the backend. A browser-supplied profile object is not evidence.
In a deployment that exposes its signing keys through this authentication API, a diagnostic check can retrieve the JSON Web Key Set without placing a user token on the command line:
curl --request GET https://auth.example.test/v1/auth/token/jwks \
--header "Accept: application/json"
Fetching keys is only one step. The Node.js verifier still has to select the matching key, verify the signature with an allowed algorithm, and enforce the expected issuer, audience, nonce, and time claims. Cache keys according to the response policy, refresh on an unknown key identifier, and fail closed if validation cannot be completed.
After validation, look up the external identity by (issuer, subject). A unique database constraint on that pair prevents one upstream identity from being attached twice. Also require a unique constraint appropriate to the local account relationship, then perform the insert and audit write in one transaction. Concurrent callbacks can happen; correctness cannot depend on which request finishes first.
For an existing password user, issue a short-lived, single-use linking intent bound to three values: the authenticated local user ID, a digest of the intended issuer-and-subject pair, and an expiry. Require recent authentication of the local account before consuming it. OWASP recommends reauthentication for sensitive account changes and after risk events; attaching a new login method belongs in that class because it changes how the account can be entered later.
The HTTP surface can remain small: one authenticated operation creates a short-lived intent, the server handles the upstream callback, and one idempotent operation confirms the mutation. Return the same result for a replay of the same completed intent. Return a generic conflict when the issuer-and-subject pair is already attached, rather than revealing which marketplace account owns it. Do not put ID tokens, authorization codes, raw email addresses, or session tokens in application logs. Keeping the contract conceptual here is intentional; route names and request fields belong to the application that enforces these invariants, not to a supposedly universal authentication API.
This is also where abuse controls belong. Rate-limit link-intent creation per account and per coarse network signal, detect repeated failed reauthentication, and require stronger verification when risk rises. A blanket challenge on every sign-in spends user attention on the high-volume path; targeted friction protects the rarer mutation that changes account access.
Count cardinality before choosing what to retain
Observability labels are an index design, not a scrapbook. result, flow, issuer_class, and a coarse risk_band have bounded value sets and work as metric dimensions. user_id, email, subject, intent_id, IP address, and user agent are effectively unbounded. Putting them in metric labels makes time-series cardinality grow with users and requests.
Count it before shipping it. With 6 results, 3 flows, 2 issuer classes, 4 risk bands, and 2 deployment regions, the planned upper bound is 6 x 3 x 2 x 4 x 2 = 288 series before status and instance dimensions. Add 2,000,000 user IDs and the bound no longer describes an operational metric. That field belongs, if justified, in a protected event store with expiration and restricted query access.
For capacity planning, use a worksheet rather than a vendor price page. At 8,000 link events per day, 1,200 bytes per event, 30 retained days, and two stored copies, the raw replicated payload is 8,000 x 1,200 x 30 x 2 = 576,000,000 bytes. Indexes, compression, metadata, and query charges depend on the storage system, so they need measurement in the actual deployment. The equation still identifies which levers matter: event count, event size, retention, and replication.
Sampling requires care. Routine successful sign-ins can be sampled for debugging after their aggregate counters are emitted. Failed link attempts, ownership conflicts, successful links, unlink operations, and administrator actions should remain complete for the defined investigation window. Tail sampling that decides after the outcome is known fits this boundary better than random head sampling, because rare security results are precisely what random sampling can discard.
What evidence earns 30 days of storage?
A useful link event answers a narrow question: who authorized which identity transition, when, under which policy decision? It does not need the credential material itself. Store an event type, timestamp, pseudonymous local-account key, keyed digest of issuer-and-subject, outcome, reason code, authentication-age bucket, coarse risk band, policy version, and correlation ID. Keep access logs for this store, too.
Thirty days is an explicit example policy boundary. It should be replaced when fraud-dispute timing, legal obligations, incident-response needs, or measured investigation latency require a different period. The important property is that the period is declared and enforced, rather than inherited accidentally from a general log index.
Separate three lifetimes:
- A linking intent lives for minutes and is deleted or invalidated after use.
- A full-fidelity security event lives for the investigation window, 30 days in this model.
- Aggregate counts without user identifiers can live longer for capacity and trend analysis.
This separation is cheaper chiefly because it stops collecting unnecessary high-volume detail. It is also safer. Data that has expired cannot be queried by an overly broad dashboard role or exposed in a later incident.
The loss is real. After the full event expires, an investigator can see that conflict rates increased but cannot reconstruct which pseudonymous account traversed a particular link decision. A support dispute filed on day 45 may have only the durable identity relation, account-security notifications, and aggregate telemetry. That is the cost of deliberate deletion. The retention owner should accept it in writing rather than pretending storage has no risk.
No hidden archive.
Test the boundary under concurrency and abuse
Unit tests are inadequate for an identity mutation. In integration tests, send two confirmations for one intent and assert that only one external identity row and one completed transition exist. Race two local accounts for the same issuer-and-subject pair. Expire the intent between validation and commit. Rotate the signing key set in a test issuer. Reject mismatched issuer, audience, nonce, and redirect state.
Then test information disclosure. An attacker should not learn whether a target email has a password account, which local account owns an external identity, or whether the owner is a buyer or seller. Responses can stay generic while internal reason codes remain specific and access-controlled.
Deployment deserves a reversible sequence: add the identity table and constraints, ship validation and audit paths, observe bounded counters, and only then expose the linking action. Alert on ratios, not raw traffic alone: conflicts per confirmation, invalid intents per attempt, and reauthentication failures per link start. Keep labels finite. A build identifier is useful in logs; an unbounded request ID is not useful as a metric dimension.
The final decision is straightforward. Use email auto-merge only in a closed system where another authoritative process has already proved that both identities belong to the same principal and the merge is an explicit, auditable operation. A public marketplace does not have that premise. Use proof-based linking, retain narrow evidence for a justified window, and delete the rest on purpose.
Top comments (0)