Skip to content

Prevent duplicate OAuth accounts after email rename (Gmail)#884

Merged
sinamics merged 3 commits intomainfrom
google
Apr 14, 2026
Merged

Prevent duplicate OAuth accounts after email rename (Gmail)#884
sinamics merged 3 commits intomainfrom
google

Conversation

@sinamics
Copy link
Copy Markdown
Owner

When a user renames their email (e.g., Gmail's new rename feature), the OAuth provider sends the updated email on next login. Previously, the sign-in callback looked up users solely by email, which would fail to find the existing account and create a duplicate, orphaning the original user and all their ZeroTier network configurations.

Resovles #882

Copilot AI review requested due to automatic review settings April 14, 2026 17:54
@github-actions github-actions Bot added the ztnet Main Application label Apr 14, 2026
@sinamics sinamics merged commit 05f022e into main Apr 14, 2026
7 checks passed
@sinamics sinamics deleted the google branch April 14, 2026 17:56
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the NextAuth sign-in callback to preferentially identify OAuth users via their stable providerAccountId (instead of email), preventing duplicate/orphaned accounts when an OAuth provider returns a renamed email address (e.g., Gmail rename).

Changes:

  • Add OAuth user lookup by (provider, providerAccountId) with an email-rename update path in signInCallback.
  • Keep email-based lookup as a fallback when no linked OAuth account exists.
  • Add Jest coverage for providerAccountId-first lookup, email update behavior, and fallback paths.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/server/callbacks/signin.ts Adds providerAccountId-based lookup and updates stored email when it changes.
src/server/api/tests/callbacks/signinOAuthLookup.test.ts Adds tests covering providerAccountId lookup, email update/no-update, fallback behavior, and user creation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +186 to +193
// Update the user's email if it changed (e.g. Gmail rename)
if (userExist && user.email && userExist.email !== user.email) {
await prisma.user.update({
where: { id: userExist.id },
data: { email: user.email },
});
userExist.email = user.email;
}
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prisma.user.update({ data: { email: user.email } }) can throw a unique-constraint error if the renamed email is already in use (including by a duplicate account created before this fix). Because it’s inside the main try/catch, that will cause the entire sign-in to return false and lock the user out even though the providerAccountId link exists. Consider catching Prisma P2002 (or doing a pre-check) and either (a) skip the email update while still allowing sign-in, or (b) return a deterministic error/redirect telling the user/admin how to resolve the conflict (e.g., merge accounts).

Copilot uses AI. Check for mistakes.
Comment on lines +44 to +45
browser: "Chrome",
os: "Linux",
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mocked parseUA return value is missing required fields (browserVersion, osVersion). This can break type-checking under ts-jest and also makes the test less representative of production behavior (those fields are required by DeviceInfo / the Prisma UserDevice model). Update the mock to include the full ParsedUA shape.

Suggested change
browser: "Chrome",
os: "Linux",
browser: "Chrome",
browserVersion: "120.0.0.0",
os: "Linux",
osVersion: "unknown",

Copilot uses AI. Check for mistakes.
Comment on lines +251 to +254
// Enable OAuth registration
process.env.OAUTH_ALLOW_NEW_USERS = "true";
process.env.OAUTH_EXCLUSIVE_LOGIN = "true";

Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test mutates process.env.OAUTH_ALLOW_NEW_USERS and process.env.OAUTH_EXCLUSIVE_LOGIN but never restores the previous values. Since Jest can run test files in any order, this can leak state into other suites and cause flaky failures. Save the old values and restore them in afterEach/afterAll (or use jest.resetModules() + per-test env setup).

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ztnet Main Application

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants