v0.1.0 — Six ecosystems

Find the OAuth handlers
that break on a Gmail rename

Since 31 March 2026, Gmail users can rename their primary address. OAuth handlers that look up users by email create a duplicate account on the next login. authdrift runs Semgrep with a narrow ruleset that finds those handlers and nothing else.

$ pip install authdrift
6
OAuth Ecosystems Covered
24
Test Fixtures
0
False Positives
124
Vulnerable Repos Found

What the ruleset covers

6
Six ecosystems, one pattern
Passport.js, NextAuth, Django or SQLAlchemy, authlib, Firebase Auth, and Lucia v3. One rule per ecosystem, each with its own vulnerable and safe fixtures.
🎯
Narrow by design
Rules require email to be used as a lookup key, not just read or logged. Display-only email, contact forms, and password resets do not match.
0
Zero false positives on the safe matrix
Eleven safe fixtures cover the most common legitimate uses of email in auth code. CI asserts zero findings on each one before any rule change ships.
🧩
Built to be extended
The authlib rule arrived as a community pull request and went through a five-point validation before merge. OmniAuth, Clerk, and Supabase Auth are open for the same treatment.

See it, then fix it

The scan tells you where the pattern lives. The fix is usually one field.

Scan a repo locally
# Install the CLI and its Semgrep dependency.
$ pip install authdrift

# Scan the current directory.
$ authdrift scan ./

src/auth/google.ts
   12:18  WARNING  oauth-passport-email-as-primary-key
            This OAuth handler is using profile.emails[0].value
            as a user lookup key. When a user renames their Gmail
            address, this lookup will fail and your application
            will silently create a duplicate user record.
            Fix: use profile.id (the OIDC sub claim)
            as the immutable primary key.

# Exit 0: clean.  Exit 1: findings.  Exit 2: error.
The fix is one field
# Before — lookup on email, breaks on rename:
const user = await db.findUser({
  email: profile.emails[0].value,   // mutable
});

# After — lookup on the OIDC sub claim, stable forever:
const user = await db.findUser({
  google_sub: profile.id,              // immutable
});

# Email still lives on the user record as a contact attribute.
# The primary key is the one thing that survives a rename.

Where the ruleset earns its place in CI

The scan is read-only and narrow. It sits alongside Semgrep, trufflehog, and gitleaks without adding noise to the diff.

🧪
Pull-request gates
Block merges that add an email-keyed lookup in a Google OAuth callback. The reviewer sees the exact file, line, and suggested fix.
🏢
Multi-tenant platforms
Audit every tenant integration for the same class of bug. One ruleset covers Passport, NextAuth, authlib, Firebase, Lucia, and Django in one pass.
🔐
Security reviews
Produce a reproducible list of every handler that will create phantom users after a rename. Evidence a reviewer can re-run in one command.
🚨
Incident response
When duplicate-account reports surface, scan the suspect branch to confirm whether the bug traces back to a known pattern or to something new.
Commercial Support

Help rolling this out on a large codebase

The OSS ruleset is enough for a fresh repo. A legacy codebase with a decade of OAuth surface often needs a hand sorting the findings and writing the migration.

🔍
Scoped audit
Run authdrift across your orgs repos and triage the findings against your actual identity model.
🧵
Custom rules
Add rules for in-house auth libraries or internal patterns that the public ruleset does not cover.
🛠️
Migration support
Pair on the sub-claim migration itself, including account-merge flows for users already duplicated by a rename.

Run the scan, read the findings, open a PR

Install in seconds. No API keys, no telemetry, no account required. MIT licensed and built on Semgrep.

$ pip install authdrift