Script Valley
JWT & Session Auth: Deep Dive
OAuth 2.0 and Third-Party AuthLesson 4.5

linking OAuth accounts to existing email-password accounts

account linking strategy, email deduplication, merging OAuth and local accounts, security risks of auto-linking, confirmation flow, multi-provider support

Linking OAuth to Existing Email-Password Accounts

OAuth Account Linking

Users often have an existing account with their email and later try to sign in with Google using the same address. Naive auto-linking is a security vulnerability.

The attack: Attacker creates a Google account with victim@example.com before the victim does. Your app auto-links on matching email. Attacker gains access to the victim's existing account.

Safe account linking approach:

  1. Check if an account with the OAuth email already exists.
  2. If the existing account has a verified email (email verification flow ran), prompt the user to link manually — require them to log in with their password first, then link the OAuth provider from account settings.
  3. If no account exists, create a new one via OAuth.
// In OAuth verify callback
const existingUser = await User.findOne({ email: profile.emails[0].value });

if (existingUser && !existingUser.googleId) {
  // Do NOT auto-link — return an error prompting manual linking
  return done(null, false, { message: 'Account exists. Log in with password to link Google.' });
}

if (existingUser && existingUser.googleId === profile.id) {
  return done(null, existingUser); // normal login
}

const newUser = await User.create({ googleId: profile.id, email: profile.emails[0].value });
return done(null, newUser);

Multi-provider support (Google + GitHub + local) requires tracking which providers each user has linked, with one canonical user record and multiple provider rows in a linked accounts table.