Firebase JS API auth - account-exists-with-different-credential

What is happening is that Firebase enforces a same account for all emails. As you already have a Google account for the same email, you need to link that Facebook account to the Google account so the user can access the same data and next time be able to sign in to the same account with either Google or Facebook.

The issue in your snippet is that you are signing and linking with the same credential. Modify as follows. When you get the error 'auth/account-exists-with-different-credential', the error will contain error.email and error.credential (Facebook OAuth credential). You need to first lookup the error.email to get the existing provider.

firebase.auth().fetchProvidersForEmail(error.email)
  .then(providers => {
    //providers returns this array -> ["google.com"]
    // You need to sign in the user to that google account
    // with the same email.
    // In a browser you can call:
    // var provider = new firebase.auth.GoogleAuthProvider();
    // provider.setCustomParameters({login_hint: error.email});
    // firebase.auth().signInWithPopup(provider)
    // If you have your own mechanism to get that token, you get it
    // for that Google email user and sign in
    firebase.auth().signInWithCredential(googleCred)
      .then(user => {
        // You can now link the pending credential from the first
        // error.
        user.linkWithCredential(error.credential)
      })
      .catch(error => log(error))

I find it strange and inconvenient that Firebase have chosen this behaviour as default, and the solution is non-trivial. Here's a full and updated solution for Firebase as of the time of writing based on @bojeil's answer.

function getProvider(providerId) {
  switch (providerId) {
    case firebase.auth.GoogleAuthProvider.PROVIDER_ID:
      return new firebase.auth.GoogleAuthProvider();
    case firebase.auth.FacebookAuthProvider.PROVIDER_ID:
      return new firebase.auth.FacebookAuthProvider();
    case firebase.auth.GithubAuthProvider.PROVIDER_ID:
      return new firebase.auth.GithubAuthProvider();
    default:
      throw new Error(`No provider implemented for ${providerId}`);
  }
}

const supportedPopupSignInMethods = [
  firebase.auth.GoogleAuthProvider.PROVIDER_ID,
  firebase.auth.FacebookAuthProvider.PROVIDER_ID,
  firebase.auth.GithubAuthProvider.PROVIDER_ID,
];

async function oauthLogin(provider) {
  try {
    await firebase.auth().signInWithPopup(provider);
  } catch (err) {
    if (err.email && err.credential && err.code === 'auth/account-exists-with-different-credential') {
      const providers = await firebase.auth().fetchSignInMethodsForEmail(err.email)
      const firstPopupProviderMethod = providers.find(p => supportedPopupSignInMethods.includes(p));

      // Test: Could this happen with email link then trying social provider?
      if (!firstPopupProviderMethod) {
        throw new Error(`Your account is linked to a provider that isn't supported.`);
      }

      const linkedProvider = getProvider(firstPopupProviderMethod);
      linkedProvider.setCustomParameters({ login_hint: err.email });

      const result = await firebase.auth().signInWithPopup(linkedProvider);
      result.user.linkWithCredential(err.credential);
    }

    // Handle errors...
    // toast.error(err.message || err.toString());
  }
}

Since google is the trusted provider for @gmail.com addresses it gets higher priority than other accounts using a gmail as their email. This is why if you sign in with Facebook then Gmail an error isn't thrown, but if you try going Gmail to Facebook then it does throw one.

See this question.

If you want to allow multiple accounts with the same email then go to the Firebase console and under Authentication -> Sign-in methods, there should be an option at the bottom to toggle this.


I've written about how to do this without needing to sign in for a second time here:

https://blog.wedport.co.uk/2020/05/29/react-native-firebase-auth-with-linking/

You need to store the original credential and retrieve to login silently before linking the accounts. Full code in link:

signInOrLink: async function (provider, credential, email) {
  this.saveCredential(provider, credential)
  await auth().signInWithCredential(credential).catch(
    async (error) => {
      try {
        if (error.code != "auth/account-exists-with-different-credential") {
          throw error;
        }
        let methods = await auth().fetchSignInMethodsForEmail(email);
        let oldCred = await this.getCredential(methods[0]);
        let prevUser = await auth().signInWithCredential(oldCred);
        auth().currentUser.linkWithCredential(credential);
      }
      catch (error) {
        throw error;
      }
    }
  );

}