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!');
}
});
}