124 Repositories. Four Ecosystems. One Broken Assumption.
I scanned 2M+ public repositories on Sourcegraph for Google OAuth handlers that key on email instead of the stable sub claim. This is what I found.
How I measured this
Two axes. Popularity on one side. Severity on the other.
Popularity is GitHub stars. Not a perfect proxy for real-world deployments but the best public signal available. A project with 30K stars has more live instances than one with 300.
Severity is how the email is used in the auth handler. Not all "email in OAuth callback" is the same problem.
- Critical: Email is the only identity key. No
subor Google ID stored. Rename guarantees a duplicate user. - High: Email is a fallback lookup key.
subis primary but email fires whensubisn't found. Rename plus revoke creates a duplicate through the fallback path. - Medium: Email is stored and displayed but
subis the actual lookup key. Rename causes stale display data. Not identity fracture. - Safe:
subis primary. Email is cosmetic. The cal.com pattern.
The full table
| Project | Stars | Ecosystem | Severity | Pattern |
|---|---|---|---|---|
| prompts.chat | 159K | NextAuth | HIGH | user.email in auth handler |
| cal.com | 41K | NextAuth | SAFE | token.sub as primary key |
| LibreChat | 35K | Passport.js | HIGH | googleId primary, email fallback |
| PostHog | 32K | Python/Django | CRITICAL | email + get_or_create |
| GitLab | 24K | Ruby/OmniAuth | HIGH | email + find_by in OmniAuth |
| Ubicloud | 12K | Ruby/OmniAuth | HIGH | email + find_or_create |
| Gumroad | 8.8K | Ruby/OmniAuth | HIGH | email + find_by |
| MeshCentral | 6.4K | Passport.js | CRITICAL | Email only. No sub reference. |
| angular-fullstack | 6K | Passport.js | HIGH | Email-keyed scaffold (code generator) |
| transfer.zip | 1.5K | Passport.js | HIGH | Email as identity key |
| CircuitVerse | 1.2K | Ruby/OmniAuth | CRITICAL | User.where(email:) only |
| ztnet | 1.1K | NextAuth | HIGH | Email in sign-in flow |
Plus 112 more below 1K stars. The full scan covered Passport.js (73 repos), NextAuth (17), Python/Django (15), Ruby/OmniAuth (19).
The danger zone
PostHog
Product analytics platform. Enterprise customers self-host it. The auth handler uses email from Google's userinfo endpoint as a key for get_or_create. Rename creates a new user record. Dashboards, feature flags, analytics data tied to the old account become inaccessible. The irony: an analytics tool built to track users that loses track of its own.
LibreChat
Open-source ChatGPT alternative. Widely self-hosted by companies. The code does the right thing first: looks up by googleId. But when that misses it falls back to email. After a rename the fallback fires with the new email. Doesn't match. Creates a new user. Chat history orphaned. I manually verified this. The fallback anti-pattern is exactly the cascade Google's own docs warn about.
GitLab
Second most used Git hosting platform. Self-hosted instances use Google OAuth for SSO. Rename fractures a developer's identity. Code reviews, merge requests, CI/CD permissions all tied to the old email. In regulated environments that's an audit trail break. GitLab.com likely has extra reconciliation logic. Self-hosted instances running the open-source version are the real exposure.
MeshCentral
Remote device management. IT departments and MSPs use it to manage thousands of machines. Email is the only identity key. No sub reference at all. If an IT admin renames their Gmail they lose access to every managed endpoint until someone manually fixes it. Security-critical context.
The propagation risk
angular-fullstack
This is a code generator. Every project scaffolded from it inherits the email-keyed OAuth pattern. The 6K stars massively undercount the blast radius. Hundreds of production apps were generated from this template. The vulnerability lives in the scaffold itself. Propagated silently into projects that never appear in the 124 count. Last commit February 2023. Effectively abandoned. The damage is already done.
The high-stakes tier
Ubicloud
Open-source cloud infrastructure. Users provision VMs and manage resources. Identity fracture here means losing access to running cloud infrastructure. Higher stakes than losing chat history.
Gumroad
Creator economy payment platform. Open-sourced in 2024. Email-keyed identity means a creator who renames their Gmail loses connection to payment history, customer list, product listings. Money is on the line.
CircuitVerse
Digital circuit simulator used in education. Students sign in with Google. The from_omniauth method looks up users exclusively by email. The uid is stored on creation but never used for lookup. Rename means a student loses access to their coursework and saved circuits.
The one that got it right
cal.com
Scheduling platform. 41K stars. Uses token.sub as the primary identity key (line 581 in their auth config). Email is used for rate limiting only. This is the pattern. This is what correct looks like. If every project in this list adopted the cal.com approach the Gmail rename would be a non-event.
Ecosystem breakdown
| Ecosystem | Email-keyed repos | Biggest projects |
|---|---|---|
| Passport.js (Google OAuth) | 73 | LibreChat (35K), MeshCentral (6.4K) |
| Ruby OmniAuth | 19 | GitLab (24K), Ubicloud (12K), Gumroad (8.8K) |
| NextAuth / Auth.js | 17 | prompts.chat (159K) |
| Python (Django/Flask) | 15 | PostHog (32K) |
Passport.js dominates the count. Almost 60% of all affected repos use it. The library itself isn't the problem. The problem is the tutorials and Stack Overflow answers that taught a generation of developers to use profile.emails[0].value as the identity key. That was correct advice until 31 March 2026.
Methodology
Sourcegraph public code search. Non-archived, non-forked repositories only. Searched for library-specific patterns in each ecosystem:
- Passport.js:
profile.emails[0].valuein files also referencingpassport-google-oauth20 - NextAuth:
user.email+findUnique/findFirst/wherein auth-related files - Python:
userinfo+email+objects.get/get_or_create - Ruby:
omniauth-google-oauth2+email+find_by/find_or_create/where
Manual verification on a sample of results. False positive rate on sampled files: 0%. One notable false negative: LibreChat's email-fallback pattern was not caught by single-file AST rules. Known limitation of the scanning approach.
cal.com was identified as a false positive in the initial Sourcegraph query. Manual inspection confirmed it uses token.sub as the primary key. Removed from the vulnerable set.
What you should do now
- Switch your primary identity key to
sub(orgoogleId/profile.iddepending on your library). Email becomes a display field. The cal.com pattern. - Add the three Semgrep rules from authdrift to your CI. Catches the email-keying pattern before it ships.
- Audit any code generators or scaffolds you use. If you scaffolded from angular-fullstack or similar templates, the vulnerability is baked into your project already. Check the auth handler.
The scanner I built for this audit is open-source: authdrift. Three Semgrep rules covering Passport.js, NextAuth, Python/Django. Run it against your own codebase in CI. It catches the patterns in this table.
The full essay explaining the Gmail rename cascade is here.