61

As I understand from the Firebase Docs, if a user authenticates his account with a credential, he should strictly login by using the same credential if the credential is not linked with another one yet.

In other words, if I create an account by using Google sign in, and then (after sign out) try to login with Facebook credential by using the same email that is used for Google credential, I should see this exception in logcat:

"An account already exists with the same email address but different sign-in credentials. Sign in using a provider associated with this email address."

And yes, I get this exception unsurprisingly. But if I create an account by using Facebook, and then try to login with Google credential, the provider of this account (Facebook) is converted to Google. This time authentication does not fail but it is not the expected result. I want to associate each user with a specific credential in a way. How should I fix this? You can see the code below:

public class SignInActivity extends AppCompatActivity implements GoogleApiClient.OnConnectionFailedListener,
        View.OnClickListener {

    private static final String TAG = "SignInActivity";
    private static final int RC_SIGN_IN = 9001;

    private GoogleApiClient mGoogleApiClient;
    private FirebaseAuth mFirebaseAuth;
    private FirebaseAuth.AuthStateListener mFirebaseAuthListener;

    private CallbackManager mCallbackManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_sign_in);

        // Facebook Login
        FacebookSdk.sdkInitialize(getApplicationContext());
        mCallbackManager = CallbackManager.Factory.create();

        LoginButton mFacebookSignInButton = (LoginButton) findViewById(R.id.facebook_login_button);
        mFacebookSignInButton.setReadPermissions("email", "public_profile");

        mFacebookSignInButton.registerCallback(mCallbackManager, new FacebookCallback<LoginResult>() {
            @Override
            public void onSuccess(LoginResult loginResult) {
                Log.d(TAG, "facebook:onSuccess:" + loginResult);
                firebaseAuthWithFacebook(loginResult.getAccessToken());
            }

            @Override
            public void onCancel() {
                Log.d(TAG, "facebook:onCancel");
            }

            @Override
            public void onError(FacebookException error) {
                Log.d(TAG, "facebook:onError", error);
            }
        });

        // Google Sign-In
        // Assign fields
        SignInButton mGoogleSignInButton = (SignInButton) findViewById(R.id.google_sign_in_button);

        // Set click listeners
        mGoogleSignInButton.setOnClickListener(this);

        GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
                .requestIdToken(getString(R.string.default_web_client_id))
                .requestEmail()
                .build();
        mGoogleApiClient = new GoogleApiClient.Builder(this)
                .enableAutoManage(this /* FragmentActivity */, this /* OnConnectionFailedListener */)
                .addApi(Auth.GOOGLE_SIGN_IN_API, gso)
                .build();

        // Initialize FirebaseAuth
        mFirebaseAuth = FirebaseAuth.getInstance();

        mFirebaseAuthListener = new FirebaseAuth.AuthStateListener() {
            @Override
            public void onAuthStateChanged(@NonNull FirebaseAuth firebaseAuth) {
                FirebaseUser user = firebaseAuth.getCurrentUser();
                if (user != null) {
                    // User is signed in
                    Log.d(TAG, "onAuthStateChanged:signed_in:" + user.getUid());
                } else {
                    // User is signed out
                    Log.d(TAG, "onAuthStateChanged:signed_out");
                }
            }
        };
    }

    @Override
    public void onStart() {
        super.onStart();
        mFirebaseAuth.addAuthStateListener(mFirebaseAuthListener);
    }

    @Override
    public void onStop() {
        super.onStop();
        if (mFirebaseAuthListener != null) {
            mFirebaseAuth.removeAuthStateListener(mFirebaseAuthListener);
        }
    }

    private void firebaseAuthWithGoogle(GoogleSignInAccount acct) {
        Log.d(TAG, "firebaseAuthWithGooogle:" + acct.getId());
        AuthCredential credential = GoogleAuthProvider.getCredential(acct.getIdToken(), null);
        mFirebaseAuth.signInWithCredential(credential)
                .addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
                    @Override
                    public void onComplete(@NonNull Task<AuthResult> task) {
                        Log.d(TAG, "signInWithCredential:onComplete:" + task.isSuccessful());

                        // If sign in fails, display a message to the user. If sign in succeeds
                        // the auth state listener will be notified and logic to handle the
                        // signed in user can be handled in the listener.
                        if (!task.isSuccessful()) {
                            Log.w(TAG, "signInWithCredential", task.getException());
                            Toast.makeText(SignInActivity.this, "Authentication failed.",
                                    Toast.LENGTH_SHORT).show();
                        } else {
                            startActivity(new Intent(SignInActivity.this, MainActivity.class));
                            finish();
                        }
                    }
                });
    }

    private void firebaseAuthWithFacebook(AccessToken token) {
        Log.d(TAG, "handleFacebookAccessToken:" + token);

        final AuthCredential credential = FacebookAuthProvider.getCredential(token.getToken());
        mFirebaseAuth.signInWithCredential(credential)
                .addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
                    @Override
                    public void onComplete(@NonNull Task<AuthResult> task) {
                        Log.d(TAG, "signInWithCredential:onComplete:" + task.isSuccessful());

                        // If sign in fails, display a message to the user. If sign in succeeds
                        // the auth state listener will be notified and logic to handle the
                        // signed in user can be handled in the listener.
                        if (!task.isSuccessful()) {
                            Log.w(TAG, "signInWithCredential", task.getException());
                            Toast.makeText(SignInActivity.this, "Authentication failed.",
                                    Toast.LENGTH_SHORT).show();
                        }

                        else {
                            startActivity(new Intent(SignInActivity.this, MainActivity.class));
                            finish();
                        }
                    }
                });
    }

    /*
    private void handleFirebaseAuthResult(AuthResult authResult) {
        if (authResult != null) {
            // Welcome the user
            FirebaseUser user = authResult.getUser();
            Toast.makeText(this, "Welcome " + user.getEmail(), Toast.LENGTH_SHORT).show();

            // Go back to the main activity
            startActivity(new Intent(this, MainActivity.class));
        }
    }
    */

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.google_sign_in_button:
                signIn();
                break;
            default:
                return;
        }
    }

    private void signIn() {
        Intent signInIntent = Auth.GoogleSignInApi.getSignInIntent(mGoogleApiClient);
        startActivityForResult(signInIntent, RC_SIGN_IN);
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        mCallbackManager.onActivityResult(requestCode, resultCode, data);

        // Result returned from launching the Intent from GoogleSignInApi.getSignInIntent(...);
        if (requestCode == RC_SIGN_IN) {
            GoogleSignInResult result = Auth.GoogleSignInApi.getSignInResultFromIntent(data);
            if (result.isSuccess()) {
                // Google Sign In was successful, authenticate with Firebase
                GoogleSignInAccount account = result.getSignInAccount();
                firebaseAuthWithGoogle(account);
            } else {
                // Google Sign In failed
                Log.e(TAG, "Google Sign In failed.");
            }
        }
    }

    @Override
    public void onConnectionFailed(@NonNull ConnectionResult connectionResult) {
        // An unresolvable error has occurred and Google APIs (including Sign-In) will not
        // be available.
        Log.d(TAG, "onConnectionFailed:" + connectionResult);
        Toast.makeText(this, "Google Play Services error.", Toast.LENGTH_SHORT).show();
    }
}
Dorukhan Arslan
  • 2,676
  • 2
  • 24
  • 42
  • 1
    Yup, checked that, but I thought there might be a workaround. For example, if you allow - multiple accounts per email address - you don't get this anymore, but users are not linked in the database. – r3dm4n Nov 27 '16 at 17:49

9 Answers9

39

Go to Authentication > Sign-in providers, click Multiple accounts per email address and Allow creation of multiple accounts with the same email address is what you are looking for.

Account email address setting

Gk Mohammad Emon
  • 6,084
  • 3
  • 42
  • 42
fluxa
  • 1,031
  • 1
  • 10
  • 10
  • 7
    Caution: It will create multiple UIDs for single user. if you are binding firebase UIDs with your local database then its not the right solution. – Muhammad Saqib Aug 05 '20 at 16:11
  • 2
    This is not what OP was looking for, or anyone. If anything, this is a mere workaround. – Hiro Dec 05 '20 at 15:06
18

Please check the thread: https://groups.google.com/forum/#!searchin/firebase-talk/liu/firebase-talk/ms_NVQem_Cw/8g7BFk1IAAAJ It explains why this happens. This is due to some security issue with Google emails being verified whereas Facebook emails are not.

bojeil
  • 29,642
  • 4
  • 69
  • 76
  • 51
    This really smells. Google automatically silently converts accounts to Google accounts, but doesn't do the same for Facebook and instead gives an error. This "trusted provider" nonsense is an excuse. No user would expect their email/password account got silently revoked. I want them to be consistent. Google should not silently convert accounts. How can I prevent this behavior when I'm in "one account per email" mode? – Greg Ennis Mar 03 '17 at 14:56
  • 3
    I implemented appleSignIn, facbookSignIn and googleSignIn with in my app and found apple and google ids do not have this problem however, only facebook having this problem. may be because the other 2 sources are considered much credible . – Amit Bravo Jan 19 '20 at 15:04
  • @GregEnnis Because there is a difference. Google is more credible because they are an identity platform as well. Whereas facebook can just verify(once) if the email belongs to the user but can't for sure know if it belongs to the user – apnerve May 28 '20 at 10:49
  • The list of trusted providers has been significantly expanded since this was posted. Also see https://stackoverflow.com/questions/60258708/trying-to-understand-firebase-authentication-one-account-per-email-address-and-t – Frank van Puffelen Mar 03 '21 at 15:21
15

I finally ended with this logic:

If user try to sign in with Facebook, but user with given email already exist (with Google provider) and this errors occures:

"An account already exists with the same email address but different sign-in credentials. Sign in using a provider associated with this email address."

So, just ask user to loging using Google (and after it silently link Facebook to existing account)

Facebook and Google Sign In logics using firebase

pupadupa
  • 1,530
  • 2
  • 17
  • 29
  • I think you meant to implement the other way round? When user signs into a Google account check if a user with that email already exists with another provider (like Facebook), and if yes, then don't log the user into Firebase, but show an error that they should use Facebook or whatever other provider instead. Right? – user1056585 Apr 25 '19 at 02:52
  • What if I want to test all the types: email, facebook, google...? – Shlomo May 18 '19 at 15:44
  • Hi, Can you pls help with this ? https://stackoverflow.com/questions/63762463/how-to-prevent-multiple-account-for-same-user-in-firebase – dReAmEr Sep 08 '20 at 06:09
  • This solves one of the problems. However it does not solve the issue when user first signs in with Facebook, then Google, then Facebook again. Google will erase the Facebook credentials, and we then have to link Facebook to the Google account by logging in with Google one more time. – Jeff Padgett Nov 30 '20 at 19:52
  • but I have 4 providers. When "FirebaseAuthUserCollisionException" occurs, there is no way to know which provider we have to show user for sign in. – Muhammad Saqib Jan 11 '21 at 20:22
9

In firebase, it is very important to verify user email account the first time they login with Facebook , by sending a verification email.

Once email is verified, you can login with both Facebook and Gmail if user is using @gmail.com as email address.

Facebook Login -> Click Link in Verification Email -> Gmail Login -> Facebook Login (OK)

Facebook Login -> Gmail Login -> Click Link in Verification Email -> Facebook Login (NOT OK)

If you did not verify the Facebook email before user logout and try to login with their gmail, you will not be able to login with Facebook again the moment they login with their gmail.

Update - If you choose to always trust Facebook emails.

You can set up a firebase function (trigger) that automatically set emailVerified to true when the first login is via a facebook account.

Sample code.

const functions = require('firebase-functions');
const admin = require('firebase-admin');

exports.app = functions.auth.user().onCreate( async (user) => {

  if (user.providerData.find(d => d && d.providerId === 'facebook.com') || user.providerData === 'facebook.com') {
    
      try {
      await admin.auth().updateUser(user.uid, {
          emailVerified: true
        })
      } catch (err) {
        console.log('err when verifying email', err)
      }
  }
})

Doc: Firebase Auth Trigger

Someone Special
  • 12,479
  • 7
  • 45
  • 76
  • This is a great answer - this will solve the issue in most cases. It still sucks that if they don't verify, it will get overridden if they use Apple or Google afterward. But this was a piece of information I had not found anywhere else - thanks. – Alex Hartford Oct 07 '20 at 17:58
  • This isn't entirely true. When a user logs in with facebook and then tries to login with google using the same email address, it will throw an error that the account is already registered. If you don't handle the error appropriately then previous facebook login will be overwritten. But firebase allows you to link the two accounts so the user can login with either one they choose. – I0_ol Mar 10 '21 at 04:27
  • If you login via Facebook, and set emailVerified = true before you login with Google account, you can login to facebook again without the error. Which is exactly what the post said. – Someone Special Mar 10 '21 at 05:01
8

To minimize the login UI clicks without compromising the account security, Firebase Authentication has a concept of 'trusted provider', where the identity provider is also the email service provider. For example, Google is the trusted provider for @gmail.com addresses, Yahoo is the trusted provider for @yahoo.com addresses, and Microsoft for @outlook.com addresses.

In the "One Account per Email address" mode, Firebase Authentication tries to link account based on email address. If a user logins from trusted provider, the user immediately signs into the account since we know the user owns the email address.

If there is an existing account with the same email address but created with non-trusted credentials (e.g. non-trusted provider or password), the previous credentials are removed for security reason. A phisher (who is not the email address owner) might create the initial account - removing the initial credential would prevent the phisher from accessing the account afterwards.

Jin Liu

Alexis Gamarra
  • 4,362
  • 1
  • 33
  • 23
  • 17
    This behavior makes me crazy. If we didn't want to trust other OAuth providers, we wouldn't enable them. By enabling Facebook, for example, I am trusting that they have verified a user's email address. That's good enough for me. I don't see how there's any potential for phishing in that scenario. – Derrick Miller Aug 15 '18 at 04:44
  • 2
    Total agree with you @DerrickMiller, google is playing a very bad game here. Did you find a way to handle the exception? – Shreeram K Mar 18 '20 at 13:48
0

As Someone Special wrote, Facebook users need email verification as well as email and password (provider = password).

I suppose if a googe email is used by a user it automatically gets verified status.

The problem that should be fixed by Google:

If the user login by Facebook and then Gmail, the account is written if he has not completed email verification. Later email verification does not change anything, the account is overwritten

If user login by Facebook and verification emal (gmail),then every think is ok. can login using both social media providers

HelloGello
  • 361
  • 1
  • 3
  • 11
-1

Allow creation of multiple accounts with the same email address is what you are looking for.

That works fine ONLY if you check the email in your backend and that's the reference for your users. If you use the Firebase Id then that won't enable to keep unique users.

Nelson La Rocca
  • 173
  • 1
  • 9
  • 2
    No it's not because you end up having different accounts with different IDs – Dani Aug 08 '20 at 13:29
  • Yeh, generally try to avoid creating multiple accounts with the same email address; it leads to lots and lots of problems down the road! – Zorayr Aug 26 '20 at 00:08
-1

I had the same issue. So I delete google account and facebook account in

firebase console > authenticate > users

that you used to logged-in in before testing one anothers.

ohm89
  • 311
  • 1
  • 3
  • 12
-11

I had the same problem, all you have to do is go to Firebase Console and then in the "Authentication" category delete the user that you want.

That works to me.

Eli Elezra
  • 65
  • 8