diff --git a/app/src/main/java/com/firebase/uidemo/auth/SignedInActivity.java b/app/src/main/java/com/firebase/uidemo/auth/SignedInActivity.java index 625e63e86..4d6b4bdb8 100644 --- a/app/src/main/java/com/firebase/uidemo/auth/SignedInActivity.java +++ b/app/src/main/java/com/firebase/uidemo/auth/SignedInActivity.java @@ -114,9 +114,8 @@ public void onClick(DialogInterface dialogInterface, int i) { } private void deleteAccount() { - FirebaseAuth.getInstance() - .getCurrentUser() - .delete() + AuthUI.getInstance() + .delete(this) .addOnCompleteListener(new OnCompleteListener() { @Override public void onComplete(@NonNull Task task) { diff --git a/auth/README.md b/auth/README.md index 06ad5faed..f7b018050 100644 --- a/auth/README.md +++ b/auth/README.md @@ -262,6 +262,32 @@ public void onClick(View v) { } ``` +### Deleting accounts + +With the integrations provided by FirebaseUI Auth, deleting a user is a multi-stage process: + +1. The user must be deleted from Firebase Auth. +2. SmartLock for Passwords must be told to delete any existing Credentials for the user, so + that they are not automatically prompted to sign in with a saved credential in the future. + +This process is encapsulated by the `AuthUI.delete()` method, which returns a `Task` representing +the entire operation: + +```java +AuthUI.getInstance() + .delete(this) + .addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + if (task.isSuccessful()) { + // Deletion succeeded + } else { + // Deletion failed + } + } + }); +``` + ### Authentication flow chart The authentication flow implemented on Android is more complex than on other diff --git a/auth/src/main/java/com/firebase/ui/auth/AuthUI.java b/auth/src/main/java/com/firebase/ui/auth/AuthUI.java index 46c245ee1..32ef8eef9 100644 --- a/auth/src/main/java/com/firebase/ui/auth/AuthUI.java +++ b/auth/src/main/java/com/firebase/ui/auth/AuthUI.java @@ -33,7 +33,9 @@ import com.firebase.ui.auth.util.GoogleApiClientTaskHelper; import com.firebase.ui.auth.util.Preconditions; import com.firebase.ui.auth.util.ProviderHelper; +import com.firebase.ui.auth.util.SmartlockUtil; import com.google.android.gms.auth.api.Auth; +import com.google.android.gms.auth.api.credentials.Credential; import com.google.android.gms.auth.api.signin.GoogleSignInOptions; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.common.api.Status; @@ -42,7 +44,9 @@ import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseUser; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; @@ -307,6 +311,56 @@ public Void then(@NonNull Task task) throws Exception { return Tasks.whenAll(disableCredentialsTask, googleSignOutTask); } + /** + * Delete the use from FirebaseAuth and delete any associated credentials from the Credentials + * API. Returns a {@code Task} that succeeds if the Firebase Auth user deletion succeeds and + * fails if the Firebase Auth deletion fails. Credentials deletion failures are handled + * silently. + * @param activity the calling {@link Activity}. + */ + public Task delete(@NonNull Activity activity) { + FirebaseUser firebaseUser = FirebaseAuth.getInstance().getCurrentUser(); + if (firebaseUser == null) { + // If the current user is null, return a failed task immediately + return Tasks.forException(new Exception("No currently signed in user.")); + } + + // Delete the Firebase user + Task deleteUserTask = firebaseUser.delete(); + + // Initialize SmartLock helper + GoogleApiClientTaskHelper gacHelper = GoogleApiClientTaskHelper.getInstance(activity); + gacHelper.getBuilder().addApi(Auth.CREDENTIALS_API); + CredentialsApiHelper credentialHelper = CredentialsApiHelper.getInstance(gacHelper); + + // Get all SmartLock credentials associated with the user + List credentials = SmartlockUtil.credentialsFromFirebaseUser(firebaseUser); + + // For each Credential in the list, create a task to delete it. + List> credentialTasks = new ArrayList<>(); + for (Credential credential : credentials) { + credentialTasks.add(credentialHelper.delete(credential)); + } + + // Create a combined task that will succeed when all credential delete operations + // have completed (even if they fail). + final Task combinedCredentialTask = Tasks.whenAll(credentialTasks); + + // Chain the Firebase Auth delete task with the combined Credentials task + // and return. + return deleteUserTask.continueWithTask(new Continuation>() { + @Override + public Task then(@NonNull Task task) throws Exception { + // Call getResult() to propagate failure by throwing an exception + // if there was one. + task.getResult(Exception.class); + + // Return the combined credential task + return combinedCredentialTask; + } + }); + } + /** * Starts the process of creating a sign in intent, with the mandatory application * context parameter. diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/account_link/SaveCredentialsActivity.java b/auth/src/main/java/com/firebase/ui/auth/ui/account_link/SaveCredentialsActivity.java index 6700ed0ea..7a1f27599 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/account_link/SaveCredentialsActivity.java +++ b/auth/src/main/java/com/firebase/ui/auth/ui/account_link/SaveCredentialsActivity.java @@ -42,6 +42,7 @@ import com.google.firebase.auth.FacebookAuthProvider; import com.google.firebase.auth.FirebaseUser; import com.google.firebase.auth.GoogleAuthProvider; +import com.google.firebase.auth.TwitterAuthProvider; public class SaveCredentialsActivity extends AppCompatBase implements GoogleApiClient.ConnectionCallbacks, ResultCallback, @@ -117,7 +118,10 @@ public void onConnected(@Nullable Bundle bundle) { translatedProvider = IdentityProviders.GOOGLE; } else if (mProvider.equals(FacebookAuthProvider.PROVIDER_ID)) { translatedProvider = IdentityProviders.FACEBOOK; + } else if (mProvider.equals(TwitterAuthProvider.PROVIDER_ID)) { + translatedProvider = IdentityProviders.TWITTER; } + if (translatedProvider != null) { builder.setAccountType(translatedProvider); } diff --git a/auth/src/main/java/com/firebase/ui/auth/util/SmartlockUtil.java b/auth/src/main/java/com/firebase/ui/auth/util/SmartlockUtil.java index 4e17d5446..c67a9d815 100644 --- a/auth/src/main/java/com/firebase/ui/auth/util/SmartlockUtil.java +++ b/auth/src/main/java/com/firebase/ui/auth/util/SmartlockUtil.java @@ -2,17 +2,33 @@ import android.app.Activity; import android.content.Intent; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.text.TextUtils; +import android.util.Log; import com.firebase.ui.auth.ui.FlowParameters; import com.firebase.ui.auth.ui.account_link.SaveCredentialsActivity; +import com.google.android.gms.auth.api.credentials.Credential; +import com.google.android.gms.auth.api.credentials.IdentityProviders; +import com.google.firebase.auth.EmailAuthProvider; +import com.google.firebase.auth.FacebookAuthProvider; import com.google.firebase.auth.FirebaseUser; +import com.google.firebase.auth.GoogleAuthProvider; +import com.google.firebase.auth.TwitterAuthProvider; +import com.google.firebase.auth.UserInfo; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; /** * Helper class to deal with Smartlock Flows. */ public class SmartlockUtil { + private static final String TAG = "SmartLockUtil"; + /** * If SmartLock is enabled and Google Play Services is available, start the save credential * Activity. Otherwise, finish the calling Activity with RESULT_OK. @@ -48,6 +64,60 @@ public static void saveCredentialOrFinish(Activity activity, activity.startActivityForResult(saveCredentialIntent, requestCode); } + /** + * Translate a Firebase Auth provider ID (such as {@link GoogleAuthProvider#PROVIDER_ID}) to + * a Credentials API account type (such as {@link IdentityProviders#GOOGLE}). + */ + public static String providerIdToAccountType(@NonNull String providerId) { + switch (providerId) { + case GoogleAuthProvider.PROVIDER_ID: + return IdentityProviders.GOOGLE; + case FacebookAuthProvider.PROVIDER_ID: + return IdentityProviders.FACEBOOK; + case TwitterAuthProvider.PROVIDER_ID: + return IdentityProviders.TWITTER; + case EmailAuthProvider.PROVIDER_ID: + // The account type for email/password creds is null + return null; + } + + return null; + } + + /** + * Make a list of {@link Credential} from a FirebaseUser. Useful for deleting Credentials, + * not for saving since we don't have access to the password. + */ + public static List credentialsFromFirebaseUser(@NonNull FirebaseUser user) { + if (TextUtils.isEmpty(user.getEmail())) { + Log.w(TAG, "Can't get credentials from user with no email: " + user); + return Collections.emptyList(); + } + + List credentials = new ArrayList<>(); + for (UserInfo userInfo : user.getProviderData()) { + // Get provider ID from Firebase Auth + String providerId = userInfo.getProviderId(); + + // Convert to Credentials API account type + String accountType = providerIdToAccountType(providerId); + + // Build and add credential + Credential.Builder builder = new Credential.Builder(user.getEmail()) + .setAccountType(accountType); + + // Null account type means password, we need to add a random password + // to make deletion succeed. + if (accountType == null) { + builder.setPassword("some_password"); + } + + credentials.add(builder.build()); + } + + return credentials; + } + private static void finishActivity(Activity activity) { activity.setResult(Activity.RESULT_OK, new Intent()); activity.finish(); diff --git a/common/constants.gradle b/common/constants.gradle index 2cf0588ca..077506cc3 100644 --- a/common/constants.gradle +++ b/common/constants.gradle @@ -3,7 +3,7 @@ project.ext.support_library_version = '23.4.0' project.ext.submodules = ['database', 'auth'] project.ext.group = "com.firebaseui" -project.ext.version = '0.5.1' +project.ext.version = '1.0.0-SNAPSHOT' project.ext.pomdesc = 'Firebase UI Android' project.ext.buildtools = '23.0.3' project.ext.compileSdk = 23