4

Requirement

1. I want to give user the option to delete his/her account, whereas of now the user can sign in using Google and a phone.

I read some documentation and it turns out that I can easily delete the account if I can reauthenticate the user, but I was not able to do that.

This is the code I am using to reauthenticate the account

Currently I am just trying with Google.

 final FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
    GoogleSignInAccount account = GoogleSignIn.getLastSignedInAccount(getActivity());
    if(account != null && user != null) {
        AuthCredential credential = GoogleAuthProvider.getCredential(account.getIdToken(),null);
        user.reauthenticate(credential)
                .addOnSuccessListener(new OnSuccessListener<Void>() {
                    @Override
                    public void onSuccess(Void aVoid) {
                        Log.d(TAG,"reauthenticated");
                    }
                })

BUT it produces an error, i.e.,

com.google.firebase.auth.FirebaseAuthInvalidCredentialsException: The supplied auth credential is malformed or has expired. [ ID Token issued at 1587271042 is stale to sign-in.

By reading some documentation I also understand if I am not wrong, this is because a token is valid for one hour and I am trying access it after one hour. That is, why am I getting this error?

I included this code so that you can tell me an alternative way.

I also know a alternative way, I tried:

By clinking the Delete account button I can start the Google sign-in flow by popping-up a Google account dialog so that the user can sign in again and because that will be a fresh sign-in, then I can just say user.delete() and it will delete the account, but it is not a good alternative for three reasons:

1 The user will be thinking why he/she has to choose an account again

2 I can not change the title of that dialog. It will always have the title choose account to continue "my app name" which doesn't reflect the my intention of deleting the account.

3 The user does not know that he/she has to choose the currently signed-in account, and he/she may choose some other account

I don't want to bother the user by taking him/her to a sign-in flow. Can I just refresh the token and delete the account right away?

Or if there isn't any way and the user has to sign in again, can I just do it somehow with AuthUI because it will be more convenient for the user and for me too as I will not have to implement a custom UI for all providers?

There are many questions related to this with zero answers. I hope this one will not fall in that category.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Abhinav Chauhan
  • 1,304
  • 1
  • 7
  • 24
  • you can do it using admin sdk. as when the user click delele account button. it ll ping an API with approriate data. which then lll start the prrocess of delteing account. the rough method is `admin.auth.user(uId).delete`. i dont remember the address of the method but it exists. thats how you should be doing it. not using reauthenticate – Harkal Apr 25 '20 at 07:36

2 Answers2

6

Using an expired authentication token won't allow you to authenticate with Firebase. So first you must get a fresh ID token.

If the GoogleSignInAccount stored on your device supports it (you have a stored refresh token), you should be able to use silentSignIn() to obtain a fresh ID token which you can then pass along to Firebase.

The below flow is roughly stamped out from JavaScript. Expect typos and bugs, but it should point you (or someone else) in the right direction.

public void deleteCurrentFirebaseUser() {
  final FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
  if (user == null) {
    // TODO: Throw error or show message to user
    return;
  }

  // STEP 1: Get a new ID token (using cached user info)
  Task<GoogleSignInAccount> task = mGoogleSignInClient.silentSignIn();
  task
    .continueWithTask(Continuation<GoogleSignInAccount, Task<AuthResult>>() {
      @Override
      public void then(Task<GoogleSignInAccount> silentSignInTask) {
        GoogleSignInAccount acct = silentSignInTask.getResult();
        // STEP 2: Use the new token to reauthenticate with Firebase
        AuthCredential credential = GoogleAuthProvider.getCredential(acct.getIdToken(), null);
        return mAuth.reauthenticate(credential);
      }
    })
    .continueWithTask(Continuation<AuthResult, Task<Void>>() {
      @Override
      public void then(Task<AuthResult> firebaseSignInTask) {
        AuthResult result = firebaseSignInTask.getResult();
        // STEP 3: If successful, delete the user
        FirebaseUser user = result.getUser();
        return user.delete();
      }
    })
    .addOnCompleteListener(this, new OnCompleteListener<Void>() {
      @Override
      public void onComplete(@NonNull Task<Void> deleteUserTask) {
        // STEP 4: Handle success/errors
        if (task.isSuccessful()) {
          // The user was successfully deleted
          Log.d(TAG, "deleteCurrentFirebaseUser:success");
          // TODO: Go to sign-in screen
        } else {
          // The user was not deleted
          // Google sign in, Firebase sign in or Firebase delete user operation failed.
          Log.w(TAG, "deleteCurrentFirebaseUser:failure", task.getException());
          Snackbar.make(mBinding.mainLayout, "Failed to delete user.", Snackbar.LENGTH_SHORT).show();

          final Exception taskEx = task.getException();
          if (taskEx instanceof ApiException) {
            ApiException apiEx = (ApiException) taskEx;
            int googleSignInStatusCode = apiEx.getStatusCode();
            // TODO: Handle Google sign-in exception based on googleSignInStatusCode
            // e.g. GoogleSignInStatusCodes.SIGN_IN_REQUIRED means the user needs to do something to allow background sign-in.
          } else if (taskEx instanceof FirebaseAuthException) {
            // One of:
            //  - FirebaseAuthInvalidUserException (disabled/deleted user)
            //  - FirebaseAuthInvalidCredentialsException (token revoked/stale)
            //  - FirebaseAuthUserCollisionException (does the user already exist? - it is likely that Google Sign In wasn't originally used to create the matching account)
            //  - FirebaseAuthRecentLoginRequiredException (need to reauthenticate user - it shouldn't occur with this flow)

            FirebaseAuthException firebaseAuthEx = (FirebaseAuthException) taskEx;
            String errorCode = firebaseAuthEx.getErrorCode(); // Contains the reason for the exception
            String message = firebaseAuthEx.getMessage();
            // TODO: Handle Firebase Auth exception based on errorCode or more instanceof checks
          } else {
            // TODO: Handle unexpected exception
          }
        }
      }
    });
}

An alternative to the above would be to use a Callable Cloud Function that uses the Admin SDK's Delete User function as commented by @example. Here's a bare-bones implementation of that (without any confirmation step):

exports.deleteMe = functions.https.onCall((data, context) => {
  if (!context.auth) {
    throw new functions.https.HttpsError('failed-precondition', 'The function must be called while authenticated.');
  }

  const uid = context.auth.uid;

  return admin.auth().deleteUser(uid)
    .then(() => {
      console.log('Successfully deleted user');
      return 'Success!';
    })
    .catch(error => {
      console.error('Error deleting user: ', error);
      throw new functions.https.HttpsError('internal', 'Failed to delete user.', error.code);
    });
});

Which would be called using:

FirebaseFunctions.getInstance()
  .getHttpsCallable("deleteMe")
  .call()
  .continueWith(new Continuation<HttpsCallableResult, Void>() {
    @Override
    public void then(@NonNull Task<HttpsCallableResult> task) {
      if (task.isSuccessful()) {
        // deleted user!
      } else {
        // failed!
      }
    }
  });

If you use the Cloud Functions approach, I highly recommend sending a confirmation email to the user's linked email address before deleting their account just to ensure it's not some bad actor. Here's a rough draft of what you would need to achieve that:

exports.deleteMe = functions.https.onCall((data, context) => {
  if (!context.auth) {
    throw new functions.https.HttpsError('failed-precondition', 'The function must be called while authenticated.');
  }

  const uid = context.auth.uid;

  return getEmailsForUser(context.auth)
    .then(userEmails => {
      if (data.email) { // If an email was provided, use that
        if (!userEmails.all.includes(data.email)) { // Throw an error if the provided email isn't linked to this user
          throw new functions.https.HttpsError('failed-precondition', 'User is not linked to provided email.');
        }
        return sendAccountDeletionConfirmationEmail(uid, data.email);
      } else if (userEmails.primary) { // If available, send confirmation to primary email
        return sendAccountDeletionConfirmationEmail(uid, userEmails.primary);
      } else if (userEmails.token) { // If not present, try the authentication token's email
        return sendAccountDeletionConfirmationEmail(uid, userEmails.token);
      } else if (userEmails.all.length == 1) { // If not present but the user has only one linked email, try that
        // If not present, send confirmation to the authentication token's email
        return sendAccountDeletionConfirmationEmail(uid, userEmails.all[0]);
      } else {
        throw new functions.https.HttpsError('internal', 'User has multiple emails linked to their account. Please provide an email to use.');
      }
    })
    .then(destEmail => {
      return {message: 'Email was sent successfully!', email: email}
    });
});

exports.confirmDelete = functions.https.onRequest((req, res) => {
  const uid = request.params.uid;
  const token = request.params.token;
  const nextPath = request.params.next;

  if (!uid) {
    res.status(400).json({error: 'Missing uid parameter'});
    return;
  }

  if (!token) {
    res.status(400).json({error: 'Missing token parameter'});
    return;
  }

  return validateToken(uid, token)
    .then(() => admin.auth().deleteUser(uid))
    .then(() => {
      console.log('Successfully deleted user');
      res.redirect('https://your-app.firebaseapp.com' + (nextPath ? decodeURIComponent(nextPath) : ''));
    })
    .catch(error => {
      console.error('Error deleting user: ', error);
      res.json({error: 'Failed to delete user'});
    });
});

function getEmailsForUser(auth) {
  return admin.auth().getUser(auth.uid)
    .then(record => {
      // Used to create array of unique emails
      const linkedEmailsMap = {};

      record.providerData.forEach(provider => {
        if (provider.email) {
          linkedEmailsMap[provider.email] = true;
        }
      });

      return {
        primary: record.email,
        token: auth.token.email || undefined,
        all: Object.keys(linkedEmailsMap);
      }
    });
}

function sendAccountDeletionConfirmationEmail(uid, destEmail) {
  const token = 'oauhdfaskljfnasoildfma'; // TODO: Create URL SAFE token generation logic

  // 'confirmation-tokens' should have the rules: { ".read": false, ".write": false }
  return admin.database().ref('confirmation-tokens/'+uid).set(token)
    .then(() => {
      // Place the UID and token in the URL, and redirect to "/" when finished (next=%2F).
      const url = `https://your-app.firebaseapp.com/api/confirmDelete?uid=${uid}&${token}&next=%2F`;

      const emailBody = 'Please click <a href="' + url + '">here</a> to confirm account deletion.<br/><br/>Or you can copy "'+url+'" to your browser manually.';

      return sendEmail(destEmail, emailBody); // TODO: Create sendEmail
    })
    .then(() => destEmail);
}

function validateToken(uid, token) {
  return admin.database().ref('confirmation-tokens/'+uid).once('value')
    .then((snapshot) => {
      if (snapshot.val() !== token) {
        throw new Error('Token mismatch!');
      }
    });
}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
samthecodingman
  • 23,122
  • 4
  • 30
  • 54
  • Thanks for your valuable answers , i am now able to do it with `GoogleSignIn.getClient(getActivity(),gso).silentSignIn()` then re-authenticate and delete although i don't know how this code knows what is current signed in account to silent sign in it again, can you please tell me – Abhinav Chauhan Apr 26 '20 at 05:23
  • although second approach you provided is better but unfortunately i was not able to with that because cloud functions are not available in java and i don't know other languages they are available in,but thanks for your guidance ,can you please also tell how do i delete user who signed in using phone auth that too again silently – Abhinav Chauhan Apr 26 '20 at 05:25
  • When you sign in a Google user in your application, the credentials are linked with your application. [`silentSignIn()`](https://developers.google.com/android/reference/com/google/android/gms/auth/api/signin/GoogleSignInClient#silentSignIn()) tries to reuse those same credentials. Regarding the part about Cloud Functions, to a client device, it doesn't matter what language you use to write them in - it is perfectly fine to use Javascript to write the Cloud Functions and Java/Kotlin/etc for the client. – samthecodingman Apr 26 '20 at 06:30
  • sorry i didn't understand the part `to a client device, it doesn't matter what language you use to write them in - it is perfectly fine to use Javascript to write the Cloud Functions and Java/Kotlin/etc` there -> https://stackoverflow.com/questions/47573981/how-to-write-firebase-cloud-functions-in-java they say we can only do in javascript in node js environment – Abhinav Chauhan Apr 26 '20 at 09:42
  • I'll rephrase: You write Cloud Functions in JavaScript that run on a container that runs Node.JS. The code running on a client device (i.e. anything that use a Firebase Client SDK that isn't a server - an android app/iOS app/website/IoT device/etc) can be written in any language you desire. If you use Java for your app, you **don't need to** also use Java just to make use of Cloud Functions. A Java app can talk to a JavaScript Cloud Function. – samthecodingman Apr 27 '20 at 06:33
  • i don't know javascript to write cloud functions, to send emails, anyway , can i also silently delete the phone auth account – Abhinav Chauhan Apr 29 '20 at 11:10
  • When you sign up a user, you should fuse the accounts together by linking them. Then when you delete the user here it would remove both sign in methods. – samthecodingman Apr 29 '20 at 11:24
  • what is fusing , use can only sign in with only one signin method, goolgle or phone – Abhinav Chauhan Apr 29 '20 at 11:26
  • Ah, I had assumed that your users could login with either their Google account or their phone to the same account. To delete a phone auth user, just perform the sign in with phone number flow again and once signed in, delete the user using `user.delete()`. – samthecodingman Apr 29 '20 at 11:56
  • sir i am able to delete phone auth account without a UI by verifying current phone number and without verification code there -> https://firebase.google.com/docs/auth/android/phone-auth#onverificationcompletedphoneauthcredential documentation says phone can be verified without code in some cases but it doesn't tell what are those cases can i assume my user will always be verified without code. – Abhinav Chauhan Apr 30 '20 at 04:48
  • 10 lines of code to sign in .. 400 lines of undocumented ceremony to delete an account .. the api seems very unbalanced IMO - simply put following the Android tutorials for this ceremony fails. Even Firebase Auth UI, that is mean't to simplify this doesn't work. – Mark May 16 '21 at 20:24
0

I dealt with the same problem and what it worked for me was this:

private GoogleSignInClient mGoogleSignInClient;

private FirebaseAuth mAuth = FirebaseAuth.getInstance();
GoogleSignInOptions gso = new
            GoogleSignInOptions.
                    Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
            .requestIdToken(getString(R.string.default_web_client_id))
            .requestEmail()
            .build();

mGoogleSignInClient = GoogleSignIn.getClient(this, gso);

// Use method silentSignIn to sign in without the choose Account Popup Dialog
mGoogleSignInClient.silentSignIn()
    .addOnCompleteListener(
        this,
        new OnCompleteListener<GoogleSignInAccount>() {
            @Override
            public void onComplete(@NonNull Task<GoogleSignInAccount> task) {

                GoogleSignInAccount acct = task.getResult();
                // Get credential and reauthenticate that Google Account
                AuthCredential credential = GoogleAuthProvider.getCredential(acct.getIdToken(), null);
                mAuth.getCurrentUser().reauthenticate(credential).addOnCompleteListener(new OnCompleteListener<Void>() {
                    @Override
                    public void onComplete(@NonNull Task<Void> task) {
                        if (task.isSuccessful()) {

                            // If reauthentication is completed, then delete the Firebase user
                            mAuth.getCurrentUser().delete()
                                .addOnCompleteListener(new OnCompleteListener<Void>() {
                                    @Override
                                    public void onComplete(@NonNull Task<Void> task) {
                                        if (task.isSuccessful()) {
                                            Intent goToSignIn = new Intent(UpdateInfoActivity.this, SignIn.class);
                                            startActivity(goToSignIn);
                                        } // End if
                                    } // End onComplete
                                });
                        }
                    } // End onComplete
                });
            } // End onComplete
        });
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Theo.b
  • 68
  • 1
  • 8