Short answer: When a shopper signs in with Google and a second account appears, first inspect the identity lookup and the transaction that creates the local account. Resolve the provider's stable subject against an identity-link table before creating a user; do not treat a matching email address as proof that two accounts belong to the same person. For an existing password account, require authentication to that account before linking. If a session may have been stolen, revoke its refresh-token family and issue a fresh family after the shopper authenticates again. Keep enough state to make that revocation effective, but put an expiration date on it.
The storage bill here is mostly state held per session and per rotation, not the identity row for one shopper. Model it before changing retention: with 2 million active sessions, retaining 30 rotation events per session means 60 million event rows, while one current-family row per session means 2 million rows. Those numbers are a capacity example, not a benchmark or a promise about byte size; indexes, replication, and retention duration determine the actual bill. The change that moves the dominant term is expiring short-lived rotation telemetry while keeping the minimum family state needed to reject a reused token.
Why do duplicate user accounts appear after adding Google login?
Picture a store with a password account holding an order history and an incoming federated assertion whose email looks identical. An application that checks only its provider-identity table, finds no match, and immediately inserts a new local user has followed a plausible code path. It has also split the shopper's identity. Email is a contact attribute; the provider subject identifies the external account within its issuer. The OpenID Connect specification defines the issuer and subject pair as the stable identifier, and explicitly warns against using email as a unique identifier for an end user.
Same email. Different identity.
Trace one login in this order: validated issuer and subject, existing external-identity row, candidate local account, proof of ownership of that local account, then the write. If the identity row is absent but a local account has the same email, pause account creation and ask the shopper to authenticate to the existing account before linking. A verified email claim can inform the interface, but it cannot substitute for that proof. An unverified claim is weaker still. The trade-off is friction at a rare boundary versus an attacker joining an account merely by presenting a familiar address. In the store's data model, an order belongs to a local user ID, so choosing the wrong local ID during that first callback affects more than the login screen: subsequent order retrieval and saved-address access would resolve against the wrong principal, and repairing the mistake later requires checking which principal actually authorized each action.
Make (issuer, subject) unique and keep a separate unique constraint on the link if the domain permits only one link per provider identity. Within one database transaction, look up the link, verify the intended local user, insert the link, and handle uniqueness conflicts by re-reading the winner. Two simultaneous callbacks can otherwise both observe an empty table and create two users. A transaction does not rescue a design with no uniqueness constraint.
The race is real.
What state earns its retention period?
Account linking and session rotation solve different problems. The link survives ordinary logouts because it maps an external identity to a local account. A refresh-token family exists so a stolen token can be detected on reuse and the affected session can be invalidated. RFC 9700 describes refresh-token rotation and reuse detection for public clients: replace the token on use, invalidate the previous one, and revoke the active token when reuse reveals a breach. The authorization server cannot tell which party presented the reused token. That uncertainty is precisely why revoking the family is preferable to guessing.
| Record | Keep while | Failure if removed too early |
|---|---|---|
| Issuer-subject link | The account remains linked | Sign-in can create a duplicate account |
| Active family and current-token verifier | The session is valid | Reuse cannot reliably revoke that session |
| Revoked-family marker | A token in that family could still be presented | A delayed stolen token may appear valid |
| Rotation events and request traces | The investigation window requires them | Less evidence for reconstructing an incident |
The fourth row is usually where storage grows fastest. Keep an operational retention policy based on the token's maximum validity, investigation needs, and applicable obligations; do not claim that one arbitrary number of days fits every system. A hash or other verifier of a refresh token is enough for comparison; storing the bearer token itself enlarges the blast radius of a database leak. Protect the family update with an atomic compare-and-swap or equivalent transaction so concurrent refreshes do not each mint an independently valid successor. The capacity example counts records, not bytes, because a wide trace row with copied claims and multiple indexes can cost much more than a compact family record; measure row and index growth separately before selecting an event-retention window, and test that cleanup never expires a revocation marker while any corresponding token remains acceptable.
Rotate without turning a duplicate account into a recovery path
The login callback should validate the federated response, resolve the external identity, and only then decide which local account receives a session. A separate linking flow starts with an authenticated local account and checks that account again at the point of linking. OWASP's authentication guidance calls for reauthentication after risk events and for careful session management; it is a useful boundary when a user reports an unfamiliar login or account split.
For the stolen-session case, locate the affected family by an opaque session identifier, mark that family revoked, and reject every subsequent refresh from it. Do not revoke every shopper session just because a duplicate user row exists: those are different failure domains. Also do not copy orders or saved payment context between user IDs on an email match. First establish ownership, then perform an auditable merge under a separate review process. Wrong-account access is harder to undo than one extra sign-in.
An implementation review should cover the losing side of each race: two first-time sign-ins, two refresh requests carrying the same token, a link attempt while another request unlinks the identity, and a late request after family revocation. Test with distinct issuer-subject pairs that share an email, plus one subject whose email changes. On deployment, migrate existing identity rows and identify collisions before enabling automatic lookup; record counts of rejected reuse, linking conflicts, and newly created users per login method without logging tokens or entire assertions. Alert on changes in those ratios, then inspect samples under restricted access.
This design charges some users an extra authentication step during linking. It also requires an indexed family record and careful transactional writes. Those costs buy a defensible answer to two questions that otherwise get conflated: which local account is this shopper, and is this particular session still trusted?
Do not conflate them.
What do we deliberately stop keeping?
Expire verbose rotation events once their incident-review window closes, and avoid retaining raw tokens altogether. Retain the issuer-subject link and the minimal revocation state for as long as their respective security decisions remain possible. The cost is real: after event expiry, an investigator may be able to establish that a family was revoked but not reconstruct every preceding refresh request. Set that window with security and operations before an incident, document the loss of detail, and test expiry against the longest accepted token lifetime. Storage savings are useful only if a delayed stolen token still fails closed.
References
- https://openid.net/specs/openid-connect-core-1_0.html
- https://www.rfc-editor.org/rfc/rfc9700.html
- https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
- https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html
Top comments (0)