How to link multiple auth providers in Firebase for Android

Firebase for Android: Dynamic Links and Invites

Firebase authentication provides a way to add user account creation and sign in capabilities to an app with a minimal amount of coding. Once a user has been authenticated with Firebase, the user is assigned a unique Firebase user ID which can be used when integrating other Firebase services such as data storage and cloud messaging.

Firebase uses the concept of authentication providers to facilitate the identification and registration of users. The list of supported Firebase authentication providers currently consists of Google, Facebook, Twitter, GitHub, phone number and email/password authentication.

Two forms of Firebase authentication are available, one involving the use of FirebaseUI Auth and the other a lower level approach using the Firebase SDK.

In practice, these involve the use of the following collection of Firebase authentication classes.

  • FirebaseAuth Instance. Much of the Firebase SDK authentication process involves the use of the FirebaseAuth shared instance. Once a reference to this object has been obtained, it can be used to perform a range of tasks such as creating accounts, signing users in and out and accessing or updating information relating to the current user.
  • AuthUI Instance. The AuthUI instance is used extensively in the FirebaseUI Auth authentication process. The class contains a range of methods including a sign-in intent builder and a sign out method.
  • FirebaseUser Class. The FirebaseUser class is used to encapsulate the profile information for the currently authenticated user. An object of this type is returned, for example, when a call is made to the getCurrentUser() method of the FirebaseAuth instance. The data stored in the object will vary depending on which authentication provider is currently being used, but typically includes information such as the user's display name, email address, a URL to a profile photo and the ID of the authentication provider used to sign into the app.
  • AuthCredential Classes. The AuthCredential class is used to encapsulate user account credentials in a way that is compatible with Firebase. This class is used when exchanging a token from a third-party authentication provider for the credentials of a Firebase account. For each authentication provider there is a corresponding AuthCredential subclass: EmailAuthCredential, PhoneAuthCredential, FacebookAuthCredential, GithubAuthCredential, GoogleAuthCredential, TwitterAuthCredential.

In this tutorial I'm going to show how to sign in via Google Sign-In API and link existed Firebase account (Google) with new one (Facebook).

Firebase Authentication with the Google Sign-In API

The Google Play Services library includes a set of APIs that provide access to a range of Google services. One such API is the Google Sign-In API which, as the name suggests, allows app developers to provide users the ability to sign into Google accounts from with an Android app. Once a user has successfully signed into a Google account, the ID token for that Google account can then be used to register the user via the Firebase authentication system. In effect this connects the user’s Google account with a corresponding Firebase authentication account, allowing the user to continue signing into the app as a Firebase registered user using the Google account details.

The core elements of Google sign-in are the GoogleSignInOptions and GoogleApiClient classes. GoogleSignInOptions is used to configured the way in which the Google sign-in operation is handled, and specifies the Google account information that is required by the Android app. GoogleApiClient provides a convenient interface for working with Google Play Services APIs without having to write extensive code and error handling logic. A GoogleApiClient instance is initialized with a suitably configured GoogleSignInOptions instance and then used to launch the Google sign-in activity. This activity takes the user through the Google sign-in process and returns the result to the app.

If the user successfully signed into a Google account using the Google user sign-in activity, the resulting data returned to the app will include the user’s Google account ID token. The Firebase SDK is then used to exchange this ID for a Firebase credential object which is, in turn, used to register the user within the Firebase authentication system and subsequently sign into the app.

To sign in users with the Google Sign-In API, you must first enable the Google sign-in method for your Firebase project:

  1. In the Firebase console, open the Authentication section.
  2. On the Sign-in Method page, enable the Google sign-in method.

Second, connect your app to Firebase. If you're using the latest version of Android Studio (version 2.2 or later), it's recommend using the Firebase Assistant to connect your app to Firebase. The Firebase Assistant can connect your existing project or create a new one for you and automatically install any necessary gradle dependencies.

To open the Firebase Assistant in Android Studio:

  1. Click Tools > Firebase to open the Assistant window.
  2. Click to expand one of the listed features (for example, Authentication), then click the provided tutorial link (for example, Email and password authentication).
  3. Click the Connect to Firebase button to connect to Firebase and add the necessary code to your app.

The Google authentication provider makes use of the Google Play Services authentication library. Within the Android Studio project tool window, locate and open the build.gradle (app: module) build file (located under Gradle Scripts) and add the library to the dependencies section of the file:

compile 'com.google.firebase:firebase-auth:11.4.2'
compile 'com.google.android.gms:play-services-auth:11.4.2'

The user interface layout is going to consist of two TextViews and two Buttons. You can see layout in below snippet

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center_horizontal">

    <TextView
        android:id="@+id/tvStatus"
        android:text="Status"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <TextView
        android:id="@+id/tvUser"
        android:text="User"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <Button
        android:id="@+id/btnSignIn"
        android:text="Sign In"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="signIn"/>

    <Button
        android:id="@+id/btnSignOut"
        android:text="Sign Out"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="signOut"/>
</LinearLayout>

Result

android_google_signin.png

Following is the MainActivity.java.

public class MainActivity extends AppCompatActivity implements
        GoogleApiClient.OnConnectionFailedListener {

    private TextView tvStatus, tvUser;
    private Button btnSignIn, btnSignOut;
    private FirebaseAuth fbAuth;
    private FirebaseAuth.AuthStateListener authListener;
    private String TAG = MainActivity.class.getSimpleName();
    private GoogleApiClient googleApiClient;
    private GoogleSignInOptions signInOptions;
    private int RC_SIGN_IN = 12345;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        tvStatus = findViewById(R.id.tvStatus);
        tvUser = findViewById(R.id.tvUser);
        btnSignIn = findViewById(R.id.btnSignIn);
        btnSignOut = findViewById(R.id.btnSignOut);

        tvStatus.setText("Signed out");

        initFirebaseAuth();
        initGoogleAuth();
    }

    private void initGoogleAuth() {
        signInOptions = new
                GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
                .requestIdToken(getString(R.string.default_web_client_id))
                .requestEmail()
                .requestProfile()
                .build();

        googleApiClient = new GoogleApiClient.Builder(this)
                .enableAutoManage(this, this)
                .addApi(Auth.GOOGLE_SIGN_IN_API, signInOptions)
                .build();
    }

    private void initFirebaseAuth() {
        fbAuth = FirebaseAuth.getInstance();

        authListener = new FirebaseAuth.AuthStateListener() {
            @Override
            public void onAuthStateChanged(@NonNull FirebaseAuth firebaseAuth) {

                FirebaseUser user = firebaseAuth.getCurrentUser();

                if (user != null) {
                    tvUser.setText(user.getEmail());
                    tvStatus.setText("Signed In");
                    Log.d(TAG, "onAuthStateChanged: " + user.getPhotoUrl());
                } else {
                    tvUser.setText("");
                    tvStatus.setText("Signed out");
                }
            }
        };
    }

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

    @Override
    public void onStart() {
        super.onStart();
        fbAuth.addAuthStateListener(authListener);
    }

    @Override
    public void onConnectionFailed(@NonNull ConnectionResult connectionResult) {
        Log.d(TAG, "onConnectionFailed: Google Play Services failure");
    }

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

        if (requestCode == RC_SIGN_IN) {
            GoogleSignInResult result =
                    Auth.GoogleSignInApi.getSignInResultFromIntent(data);

            if (result.isSuccess()) {
                GoogleSignInAccount account = result.getSignInAccount();
                authWithFirebase(account);
            } else {
                Log.d(TAG, "onActivityResult: Google sign-in failed.");
            }
        }
    }

    public void signIn(View view) {
        Intent signInIntent = Auth.GoogleSignInApi.getSignInIntent(googleApiClient);
        startActivityForResult(signInIntent, RC_SIGN_IN);
    }

    public void signOut(View view) {
        fbAuth.signOut();

        Auth.GoogleSignInApi.signOut(googleApiClient).setResultCallback(
                new ResultCallback<Status>() {
                    @Override
                    public void onResult(@NonNull Status status) {}
                });
    }

    private void authWithFirebase(GoogleSignInAccount acct) {
        AuthCredential credential = GoogleAuthProvider.getCredential(
                acct.getIdToken(), null);

        fbAuth.signInWithCredential(credential)
                .addOnCompleteListener(this,
                        new OnCompleteListener<AuthResult>() {
                            @Override
                            public void onComplete(@NonNull Task<AuthResult> task) {
                                if (!task.isSuccessful()) {
                                    Log.d(TAG, "Firebase authentication failed.");
                                }
                            }
                        });
    }

}

Linking and Unlinking Firebase Authentication Providers

Firebase account linking allows users to sign into the same account using different authentication providers. By linking the user’s Facebook and Twitter credentials, for example, the user can sign into the same account using either sign-in provider. Having linked these accounts, the user is then able to sign into the app using either of those accounts.

Consider, for the purposes of an example, a user registered to access an app using an account created via the Firebase Facebook authentication provider. If the user creates a new account using the Google authentication provider and links that new account to the original Facebook-based account, either account can then be used to sign into the app.

Limitations of Firebase Account Linking. When using account linking it is important to be aware that some limitations exist. First, only two accounts can participate in a link. If an attempt is made to link to an account which is already linked, the new link will replace the original link.

It is also not possible to link two accounts associated with the same authentication provider. While a Facebook account may be linked with a Google account, for example, it is not possible to link two Google provider based accounts. An attempt to link accounts from the same provider will result in an exception containing a message which reads as follows: User has already been linked to the given provider.

Account linking can only be performed at the point at which a new account is created. It is not possible, in other words, to link two pre-existing accounts. A workaround to this limitation is to delete one of the two accounts and then establish the link while re-creating the account.

All that is required is the AuthCredential object for the new account and the FirebaseUser instance for the account with which the link is to be established.

First of all, create Facebook login form as described in Authenticate Using Facebook Login.

public class MainActivity extends AppCompatActivity implements
        GoogleApiClient.OnConnectionFailedListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ...
        initFb();
    }

    private void initFb() {
        callbackManager = CallbackManager.Factory.create();

        tvLink = findViewById(R.id.tvLink);

        btnLogin = findViewById(R.id.btnLogin);
        btnLogin.setReadPermissions("public_profile");
        btnLogin.registerCallback(callbackManager, new FacebookCallback<LoginResult>() {
            @Override
            public void onSuccess(LoginResult loginResult) {
                AccessToken token = loginResult.getAccessToken();
                Log.d(TAG, "onSuccess: " + token.getUserId());
            }

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

            @Override
            public void onError(FacebookException e) {
                Log.d(TAG, "onError: Login attempt failed.");
            }
        });
    }

    public void linkFb(View v) {
        AccessToken accessToken = AccessToken.getCurrentAccessToken();
        AuthCredential credential = FacebookAuthProvider.getCredential(accessToken.getToken());

        fbAuth.getCurrentUser().linkWithCredential(credential)
                .addOnCompleteListener(activity,
                        new OnCompleteListener<AuthResult>() {
                            @Override
                            public void onComplete(@NonNull Task<AuthResult> task) {
                                if (!task.isSuccessful()) {
                                    Toast.makeText(activity, "Authentication failed.", Toast.LENGTH_SHORT).show();
                                    Log.d(TAG, "onComplete: " + task.getException().getMessage());
                                } else {
                                    FirebaseUser user = task.getResult().getUser();

                                    StringBuilder sb = new StringBuilder("Name: " + user.getDisplayName());
                                    sb.append("\nProvider: " + user.getProviderId());

                                    tvLink.setText(sb.toString());
                                }
                            }
                        });

    }
}

Result

android_firebase_linking.png

Unlinking an Authentication Provider

An existing link can be removed from an account by calling the unlink() method of the FirebaseUser object for the current user, passing through the provider ID of the account to be unlinked.

A list of provider IDs associated with an account can be obtained by calling the getProviderData() method of the current user’s FirebaseUser object as follows:

FirebaseUser user = fbAuth.getCurrentUser();       
List<? extends UserInfo> providerData = user.getProviderData();

The above method call returns a list of UserInfo objects, each containing the provider for the corresponding account. The following code iterates through the UserInfo objects in the above providerData list and outputs the provider ID for each to the console:

for (UserInfo userInfo : providerData ) {
    String providerId = userInfo.getProviderId();
    Log.d(TAG, "providerId = " + providerId);
}

Unlinking code should be implemented such that it identifies a specific provider and then unlinks it from the current user account. The following code, for example, unlinks the user’s Google provider-based account:

for (UserInfo userInfo : providerData ) {
    String providerId = userInfo.getProviderId();
    Log.d(TAG, "providerId = " + userInfo.getProviderId());

    if (providerId.equals("google.com")) {
        user.unlink(providerId)
            .addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
                @Override
                public void onComplete(@NonNull Task<AuthResult> task) {
                    if (!task.isSuccessful()) {
                        // Handle error
                    }
                }
            });
    }
} 

Once the Google authentication provider account has been unlinked, the user will no longer be able to sign into the app using those credentials leaving only the original account available for signing in.

Unlinking the only authentication provider registered for a user account will turn the account into an anonymous account. As such, the user will only have access to the account and associated Firebase stored data for the remainder of the current log in session and once the user logs out, that access will be lost. The user should either be prompted to link a different provider based account to the anonymous account before logging out or, more preferably, prevented from unlinking the last remaining authentication provider.