Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions src/authdrift/rules/oauth-email-key-extended.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
rules:
- id: authlib-google-oauth-email-as-primary-key
message: |
This authlib OAuth handler is using `userinfo['email']` as a user lookup key.

When a user renames their Gmail address (Google rolled out Gmail rename in
March 2026), the email returned by Google changes and your application will
silently create a duplicate user record. The same drift occurs in ~0.04% of
normal Sign-in-with-Google logins (Truffle Security, January 2025).

Fix: use `userinfo['sub']` (the OIDC subject claim) as the immutable primary
key. Treat `userinfo['email']` as a mutable contact attribute, not an
identifier.

See: https://github.com/Neelagiri65/authdrift#why-this-matters
severity: WARNING
languages:
- python
patterns:
- pattern-either:
- pattern: $MODEL.objects.get(email=$UI['email'])
- pattern: $MODEL.objects.filter(email=$UI['email'])
- pattern: $MODEL.objects.get(email=$UI.get('email'))
- pattern: $MODEL.query.filter_by(email=$UI['email'])
- pattern: db.session.query($MODEL).filter_by(email=$UI['email'])

- id: firebase-auth-getUserByEmail
message: |
This code uses `getUserByEmail()` to resolve a Firebase Auth user.

When a user renames their Gmail address (Google rolled out Gmail rename in
March 2026), the email returned by the identity provider changes and your
application will create a duplicate user record on next sign-in.

Fix: use `getUserByUid()` or key on `user.uid` (the immutable Firebase
subject ID). Treat email as a mutable contact attribute.

See: https://github.com/Neelagiri65/authdrift#why-this-matters
severity: WARNING
languages:
- javascript
- typescript
patterns:
- pattern-either:
- pattern: admin.auth().getUserByEmail($EMAIL)
- pattern: getAuth().getUserByEmail($EMAIL)
- pattern: $AUTH.getUserByEmail($EMAIL)

- id: lucia-auth-email-as-primary-key
message: |
This Lucia auth flow is resolving users by email instead of the provider's
immutable subject ID.

When a user renames their Gmail address (Google rolled out Gmail rename in
March 2026), the email returned by the provider changes and a duplicate user
record will be created on next sign-in.

Fix: key the lookup on the provider's `providerUserId` (OIDC `sub` claim)
and treat email as a mutable contact attribute.

See: https://github.com/Neelagiri65/authdrift#why-this-matters
severity: WARNING
languages:
- javascript
- typescript
patterns:
- pattern-either:
- pattern: db.table("user").where("email", "=", $EMAIL)
- pattern: db.getUserByEmail($EMAIL)
28 changes: 28 additions & 0 deletions tests/fixtures/authlib-safe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Should NOT trigger any rules — using sub as primary key
from authlib.integrations.flask_client import OAuth
from flask import Flask
from myapp.models import User

app = Flask(__name__)
oauth = OAuth(app)

google = oauth.register(
name="google",
client_id="...",
client_secret="...",
server_metadata_url="https://accounts.google.com/.well-known/openid-configuration",
client_kwargs={"scope": "openid email profile"},
)


@app.route("/auth/callback")
def auth_callback():
token = google.authorize_access_token()
userinfo = token.get("userinfo")

# SAFE: using sub (OIDC subject ID) as immutable primary key
user = User.objects.get(provider="google", provider_uid=userinfo["sub"])
# email is stored as mutable contact attribute only
user.email = userinfo.get("email", "")
user.save()
return user
25 changes: 25 additions & 0 deletions tests/fixtures/authlib-vulnerable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Should trigger authlib-google-oauth-email-as-primary-key
from authlib.integrations.flask_client import OAuth
from flask import Flask
from myapp.models import User

app = Flask(__name__)
oauth = OAuth(app)

google = oauth.register(
name="google",
client_id="...",
client_secret="...",
server_metadata_url="https://accounts.google.com/.well-known/openid-configuration",
client_kwargs={"scope": "openid email profile"},
)


@app.route("/auth/callback")
def auth_callback():
token = google.authorize_access_token()
userinfo = token.get("userinfo")

# VULNERABLE: using email as primary lookup key
user = User.objects.get(email=userinfo["email"])
return user
8 changes: 8 additions & 0 deletions tests/fixtures/firebase-vulnerable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Should trigger firebase-auth-getUserByEmail
const admin = require("firebase-admin");

async function handleSignIn(email) {
// VULNERABLE: resolving user by email instead of UID
const user = await admin.auth().getUserByEmail(email);
return user;
}
Loading