Skip to content

SmartLock Deletion #302

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 14, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<Void>() {
@Override
public void onComplete(@NonNull Task<Void> task) {
Expand Down
26 changes: 26 additions & 0 deletions auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Void>() {
@Override
public void onComplete(@NonNull Task<Void> task) {
if (task.isSuccessful()) {
// Deletion succeeded
} else {
// Deletion failed
}
}
});
```

### Authentication flow chart

The authentication flow implemented on Android is more complex than on other
Expand Down
54 changes: 54 additions & 0 deletions auth/src/main/java/com/firebase/ui/auth/AuthUI.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -307,6 +311,56 @@ public Void then(@NonNull Task<GoogleApiClient> 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<Void> 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<Void> 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<Credential> credentials = SmartlockUtil.credentialsFromFirebaseUser(firebaseUser);

// For each Credential in the list, create a task to delete it.
List<Task<?>> 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<Void> combinedCredentialTask = Tasks.whenAll(credentialTasks);

// Chain the Firebase Auth delete task with the combined Credentials task
// and return.
return deleteUserTask.continueWithTask(new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(@NonNull Task<Void> 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Status>,
Expand Down Expand Up @@ -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);
}
Expand Down
70 changes: 70 additions & 0 deletions auth/src/main/java/com/firebase/ui/auth/util/SmartlockUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<Credential> 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<Credential> 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();
Expand Down
2 changes: 1 addition & 1 deletion common/constants.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down