I am trying to implement a function to delete current user's account on iOS. Account deletion works properly but the problem is that I cannot delete the account's data from Database and Storage when deleting an account.
"currentUser.delete" deletes the account but I think there is no authentication to delete its data from Database and Storage. Permission denied message shows up in the log. After running this function, I get to see the account is gone in Firebase Console Authentication page but data from Database and Storage persists.
Is this the correct way to delete an account?
I tried to delete data from Database and Storage before deleting the account. However, Firebase asks for re-authentication if session is more than 5 minutes old. Re-login shows empty data to the user before performing account deletion again so this is misleading and very confusing.
Please let me know how to remove data when deleting an account.
private func deleteAccount() {
guard let currentUser = Auth.auth().currentUser else {
return print("user not logged in")
}
currentUser.delete { error in
if error == nil {
// 1. Delete currentUser's data from Database. Permission denied
// 2. Delete currentUser's data from Storage. Permission denied
// present login screen (welcome page)
self.presentLoginScreen()
} else {
guard let errorCode = AuthErrorCode(rawValue: error!._code) else { return }
if errorCode == AuthErrorCode.requiresRecentLogin {
self.showMessage("Please re-authenticate to delete your account.", type: .error)
do {
try Auth.auth().signOut()
self.presentLoginScreen()
} catch {
print("There was a problem logging out")
}
}
}
}
}
Swift 5 | Firebase 8.11.0
To solve the problems that you've mentioned (delete the data before deleting the actual user and potentially get the AuthErrorCode.requiresRecentLogin error), you may use DispatchGroup and check the lastSignInDate, like this (just call deleteUserProcess()):
let deleteDataGroup = DispatchGroup()
func deleteUserProcess() {
guard let currentUser = Auth.auth().currentUser else { return }
deleteUserData(user: currentUser)
// Call deleteUser only when all data has been deleted
deleteDataGroup.notify(queue: .main) {
self.deleteUser(user: currentUser)
}
}
/// Remove data from Database & Storage
func deleteUserData(user currentUser: User) {
// Check if `currentUser.delete()` won't require re-authentication
if let lastSignInDate = currentUser.metadata.lastSignInDate,
lastSignInDate.minutes(from: Date()) >= -5 {
deleteDataGroup.enter()
Database.database().reference().child(userId).removeValue { error, _ in
if let error = error { print(error) }
self.deleteDataGroup.leave()
}
// Delete folders from Storage isn't possible,
// so list and run over all files to delete each one independently
deleteDataGroup.enter()
Storage.storage().reference().child(userId).listAll { list, error in
if let error = error { print(error) }
list.items.forEach({ file in
self.deleteDataGroup.enter()
file.delete { error in
if let error = error { print(error) }
self.deleteDataGroup.leave()
}
})
deleteDataGroup.leave()
}
}
}
/// Delete user
func deleteUser(user currentUser: User) {
currentUser.delete { error in
if let error = error {
if AuthErrorCode(rawValue: error._code) == .requiresRecentLogin {
reauthenticate()
} else {
// Another error occurred
}
return
}
// Logout properly
try? Auth.auth().signOut()
GIDSignIn.sharedInstance.signOut()
LoginManager().logOut()
// The user has been deleted successfully
// TODO: Redirect to the login UI
}
}
func reauthenticate() {
// TODO: Display some UI to get credential from the user
let credential = ... // Complete from https://stackoverflow.com/a/38253448/8157190
Auth.auth().currentUser?.reauthenticate(with: credential) { _, error in
if let error = error {
print(error)
return
}
// Reload user (to update metadata.lastSignInDate)
Auth.auth().currentUser?.reload { error in
if let error = error {
print(error)
return
}
// TODO: Dismiss UI
// Call `deleteUserProcess()` again, this time it will delete the user
deleteUserProcess()
}
}
}
The minuets function can be added in an extension to Date (thanks to Leo Dabus):
extension Date {
/// Returns the amount of minutes from another date
func minutes(from date: Date) -> Int {
return Calendar.current.dateComponents([.minute], from: date, to: self).minute ?? 0
}
}
you can first make your specific user deleted and and its value through its UID then you can deleted user and take him to root view controller or login screen after deleting it.
// removing user data from firebase and its specific user id
let user = Auth.auth().currentUser
user?.delete { error in
if let error = error {
// An error happened.
print(error.localizedDescription)
} else {
Database.database().reference().child("users").child(user?.uid ?? "").removeValue()
self.navigationController?.popToRootViewController(animated: true)
// Account deleted and logout user
// do {
// try Auth.auth().signOut()
// take you to root
// self.navigationController?.popToRootViewController(animated: true)
}
Related
I want user to login once and not have to reenter their login info everytime they open app unless they logout in the last session.
Login screen is currently displayed everytime the app is open. This is my rootview
struct AppRootView: View {
var body: some View {
AnyView {
// check if user has already logged in here and then route them accordingly
if auth.token != nil {
homeMainView()
} else {
LoginController()
}
}
}
}
currently this is what I use to login users
#objc func signUp() {
setLoading(true);
app.usernamePasswordProviderClient().registerEmail(username!, password: password!, completion: {[weak self](error) in
// Completion handlers are not necessarily called on the UI thread.
// This call to DispatchQueue.main.sync ensures that any changes to the UI,
// namely disabling the loading indicator and navigating to the next page,
// are handled on the UI thread:
DispatchQueue.main.sync {
self!.setLoading(false);
guard error == nil else {
print("Signup failed: \(error!)")
self!.errorLabel.text = "Signup failed: \(error!.localizedDescription)"
return
}
print("Signup successful!")
// Registering just registers. Now we need to sign in, but we can reuse the existing username and password.
self!.errorLabel.text = "Signup successful! Signing in..."
self!.signIn()
}
})
}
#objc func signIn() {
print("Log in as user: \(username!)");
setLoading(true);
app.login(withCredential: AppCredentials(username: username!, password: password!)) { [weak self](maybeUser, error) in
DispatchQueue.main.sync {
self!.setLoading(false);
guard error == nil else {
// Auth error: user already exists? Try logging in as that user.
print("Login failed: \(error!)");
self!.errorLabel.text = "Login failed: \(error!.localizedDescription)"
return
}
guard let user = maybeUser else {
fatalError("Invalid user object?")
}
print("Login succeeded!");
//
let hostingController = UIHostingController(rootView: ContentView())
self?.navigationController?.pushViewController(hostingController, animated: true)
}
how could I implement one time login so that users do have to login each time they open the app?
A correctly configured and initialized RealmApp class will persist the session information for you between app restarts, you can check for an existing session using the .currentUser() method from this class. So in your case something like:
if app.currentUser() != nil {
homeMainView()
} else {
LoginController()
}
While using Realm to persist login is a good idea, but I would highly
advice against using it for managing user authentication credentials such
as passwords. A better approach if you want to save sensitive information is
using KeyChain just like what Apple and password manager apps do. With a light
weight keyChain wrapper library such as SwiftKeychainWrapper You can easily
save your login credentials in the most secure way.
Here is a sample using a keyChain wrapper linked above.
With simple modification you can use this helper class to manage your sign in credentials anywhere in your app.
import SwiftKeychainWrapper
class KeyChainService {
// Make a singleton
static let shared = KeyChainService()
// Strings which will be used to map data in keychain
private let passwordKey = "passwordKey"
private let emailKey = "emailKey"
private let signInTokenKey = "signInTokenKey"
// Saving sign in info to keyChain
func saveUserSignInInformation(
email: String,
password: String,
token: String
onError: #escaping() -> Void,
onSuccess: #escaping() -> Void
) {
DispatchQueue.global(qos: .default).async {
let passwordIsSaved: Bool = KeychainWrapper.standard.set(password, forKey: self.passwordKey)
let emailIsSaved: Bool = KeychainWrapper.standard.set(email, forKey: self.emailKey)
let tokenIsSaved: Bool = KeychainWrapper.standard.set(token, forKey: self.signInTokenKey)
DispatchQueue.main.async {
// Verify that everything is saved as expected.
if passwordIsSaved && emailIsSaved && tokenIsSaved {
onSuccess()
}else {
onError()
}
}
}
}
// Retrieve signIn information for auto login
func retrieveSignInInfo(onError: #escaping() -> Void, onSuccess: #escaping(UserModel) -> Void) {
DispatchQueue.main.async {
let retrievedPassword: String? = KeychainWrapper.standard.string(forKey: self.passwordKey)
let retrievedEmail: String? = KeychainWrapper.standard.string(forKey: self.emailKey)
let retrievedToken: String? = KeychainWrapper.standard.string(forKey: self.signInTokenKey)
if let password = retrievedPassword,
let email = retrievedEmail,
let token = retrievedToken {
// Assuming that you have a custom user model named "UserModel"
let user = UserModel(email: email, password: password,token: token)
// Here is your user info which you can use to verify with server if needed and auto login user.
onSuccess(user)
}else {
onError()
}
}
}
}
i just want to update authenticate email address of current user. i have tried lot's of solution like updateEmail method of firebase but it not work !! if any one know then please tell me how can i achieved this Thanks in advance !!
#IBAction func btnResetEmailClick(_ sender: UIButton) {
let auth = Auth.auth()
guard let email = self.txtEmailAddress.text ?? auth.currentUser?.email else { return }
// email that i have to update with current user email
auth.currentUser?.updateEmail(to: (auth.currentUser?.email)!, completion: { (error) in
if error == nil{
}else{
}
})
}
To change the email address the user has to be logged in recently i would suggest doing this:
var credential: AuthCredential
#IBAction func changeEmail() {
if let user = Auth.auth().currentUser {
// re authenticate the user
user.reauthenticate(with: credential) { error in
if let error = error {
// An error happened.
} else {
// User re-authenticated.
user.updateEmail(to: "email") { (error) in
// email updated
}
}
}
}
}
This is effective method to solve it.
let user = Auth.auth().currentUser
user?.updateEmail(to: "email") { error in
if error != nil {
// An error happened
} else {
// Email updated.
}
}
I am creating an application which authenticate user using PhoneAuth. In my application I have a function which let user add Email to his account But not meant that I authenticate user using Email and Password, I just want to add email to his/her account (auth.auth().currentUser).
Initially, I let user to add his/her email in textfield and then I start to logout user from his/her device in order to reauthentication otherwise, I cannot update user's email using auth.updateEmail(). But sadly, the credential always expired after I called func updateEmail().
This is how I signIn user and update Email
let credential = PhoneAuthProvider.provider().credential(withVerificationID: verficationID, verificationCode: code)
Auth.auth().signInAndRetrieveData(with: credential) { (result, error) in
guard let result = result else {
completion(false)
return
}
if error == nil {
guard let user = Auth.auth().currentUser else {
return
}
if UserDefaults.standard.getUserUpdatedEmail() {
user.reauthenticate(with: credential, completion: { (error) in
if error == nil {
user.updateEmail(to: newEmail, completion: { (error) in
if error == nil {
UserDefaults.standard.setUserUpdatedEmail(value: false)
completion(true)
//return true
} else {
completion(false)
//return false
print("Error validate email ",error?.localizedDescription ?? "")
}
})
} else {
completion(false)
// return false
print("Error reauthntication email ",error?.localizedDescription ?? "")
}
})
} else {
print("User is signed in \(result.user)")
print("This is userID \(result.user.uid)")
completion(true)
}
} else {
if let error = error {
print("Error during verification \(error.localizedDescription)")
}
completion(false)
}
}
I don't why the credential is expired too fast? I cannot figure it out how to update user email using PhoneAuthCredential. Is there any other techniques to do it?
You are trying to re-use the same phone credential (you used it first to sign-in). The phone credential is one time use only. If you want to update email immediately after sign-in, re-auth is not needed. However, if you try to update email after some time, you will need to send a new SMS code to re-authenticate.
So in my app I just made it so that the user can choose to delete their account, and that works out beautifully. However after the account is deleted the app crashes. I think this is because it is trying to search for a user but it's not there.
Here is my code:
let loginController = LoginController()
func deleteAccount() {
let user = Auth.auth().currentUser
let userId = Auth.auth().currentUser?.uid
let databaseUser = Database.database().reference().child("users").child(userId!)
user?.delete { error in
if let error = error {
print(error)
} else {
self.present(self.loginController, animated: true, completion: nil)
}
}
databaseUser.removeValue(completionBlock: { (error, ref) in
if error != nil {
print(error)
} else {
self.present(self.loginController, animated: true, completion: nil)
} //Without doing this the user's account only gets deleted in the Authentication, not the whole database. I think this is the problem here?
})
}
Thank you so much in advance!
First you need to delete the users database as to do that you would need the let userId = Auth.auth().currentUser?.uid which is only active if the user itself is on your backend, Then you go on to delete the auth.
func deleteAccount() {
let user = Auth.auth().currentUser
let userId = Auth.auth().currentUser!.uid
let databaseUser = Database.database().reference().child("users").child(userId)
databaseUser.removeValue(completionBlock: { (error, ref) in
if error != nil {
print(error)
} else {
user?.delete { error in
if let error = error {
print(error)
} else {
self.present(self.loginController, animated: true, completion: nil)
}
}
})
}
If this still doesn't work track down the lifecycle of the user using debugging tools....
You are implicitly unwrapping an optional in this line with !:
let databaseUser = Database.database().reference().child("users").child(userId!)
You should check whether it's nil in the first place with a guard statement:
guard let userId = Auth.auth().currentUser?.uid else {
return
}
let databaseUser = Database.database().reference().child("users").child(userId)
[...]
Moreover, your code logic is likely to be wrong, as you are getting nil for the userId before you can work with it.
The code order is not ideal because deleting the user also logs them out. So the code may be trying to access the users node after the user was logged out.
Also remember that Firebase is asynchronous and the only way to know a function has completed is when the code inside the closure executes i.e. in this case the databaseUser.removeValue may be firing before the delete user or sometimes it may not.
Code is faster than the internet so it's best to leverage the closures so you know when it's safe to proceed.
Try this sequence; noting that we don't try to delete the Firebase user until we know for sure the data in the users node was deleted. There could use more error checking but you get the idea.
let userRef = self.ref.child("users").child(uid)
userRef.setValue(nil, withCompletionBlock: { snapshot in
Auth.auth().currentUser?.delete(completion: { err in
if err != nil {
print(err?.localizedDescription)
}
})
})
I want to delete my current user from Firebase. The authenticated user gets deleted however, I am unable to delete the data for that user in the database. What am i doing wrong?
This is my delete user method....
FIRAuth.auth()?.signIn(withEmail: (emailTextField?.text)! , password: (passwordTextField?.text)!, completion: { (user, error) in
if error == nil {
print("User Authenticate!!!")
let user = FIRAuth.auth()?.currentUser
user?.delete(completion: { (error) in
if error != nil {
print("Error unable to delete user")
} else {
DataService.ds.deleteCurrentFirebaseDBUser()
KeychainWrapper.standard.removeObject(forKey: KEY_UID)
self.performSegue(withIdentifier: "goToLogin", sender: nil)
}
})
} else {
//Password was wrong, unable to authenicate user. Data is not updated
print("!!!ALERT!!! Unable to authenticate user")
let alert = UIAlertController(title: "Incorrect Password", message: "Please re-enter your password", preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
})
Firebase Rules:
{
"rules": {
"users": {
"$uid": {
".read": "$uid === auth.uid",
".write": "$uid === auth.uid"
}
}
}
}
Database:
App
-> users
->
4erkjkl543jfe46
->name
->email
ERRORS:
2017-01-21 21:33:10.321704 APP[11582:4102711] [FirebaseDatabase] setValue: or removeValue: at /users/4erkjkl543jfe46 failed: permission_denied
Optional(Error Domain=com.firebase Code=1 "Permission denied" UserInfo={NSLocalizedDescription=Permission denied})
I'm having the same issue. You are not able to make use of your function deleteCurrentFirebaseDBUser() because the Firebase delete function (if successful) removes the user auth object.
As a result user is not authenticated anymore at the time you want to delete user's data in database with deleteCurrentFirebaseDBUser().
Currently I delete user's data in database before Firebase delete function which is not the ideal solution.
We can delete user from both side authentication and database.But before that we need to reauthenticate user first then we get latest token to delete the user.
Here is the pretty code:
let user = Auth.auth().currentUser
user?.reauthenticate(with:credential) { error in
if let error = error {
// An error happened.
showAlertWithErrorMessage(message: error.localizedDescription)
} else {
// User re-authenticated.
user?.delete { error in
if let error = error {
// An error happened.
showAlertWithErrorMessage(message: error.localizedDescription)
} else {
// Account deleted.
let userID = HelperFunction.helper.FetchFromUserDefault(name: kUID)
Database.database().reference(fromURL: kFirebaseLink).child(kUser).child(userID).removeValue()
try! Auth.auth().signOut()
showAlertWithErrorMessage(message: "Your account deleted successfully...")
return
}
}
}
}
100% working in my project and well tested
for just to delete a child from Firebase use "removeValue()"
var db: DatabaseReference!
override func viewDidLoad() {
super.viewDidLoad()
db = Database.database().reference()
deleteByID()
}
func deleteByID(){
db.child("YOURID").removeValue()
}
Swift 5 | Firebase 8.11.0
As #SvshX said, deleting the user data before deleting the actual user is the only available solution.
The problem with this method is that deleting the user might give an error like AuthErrorCode.requiresRecentLogin, then the data will be deleted but the user will not.
This error is given when the last authentication of the user was more than 5 minuets ago (from Firebase Docs)
So, fixing both of the issues can be achieved by using DispatchGroup and checking the lastSignInDate.
This is my final solution (just call deleteUserProcess()):
let deleteDataGroup = DispatchGroup()
func deleteUserProcess() {
guard let currentUser = Auth.auth().currentUser else { return }
deleteUserData(user: currentUser)
// Call deleteUser only when all data has been deleted
deleteDataGroup.notify(queue: .main) {
self.deleteUser(user: currentUser)
}
}
/// Remove data from Database & Storage
func deleteUserData(user currentUser: User) {
// Check if `currentUser.delete()` won't require re-authentication
if let lastSignInDate = currentUser.metadata.lastSignInDate,
lastSignInDate.minutes(from: Date()) >= -5 {
deleteDataGroup.enter()
Database.database().reference().child(userId).removeValue { error, _ in
if let error = error { print(error) }
self.deleteDataGroup.leave()
}
// Delete folders from Storage isn't possible,
// so list and run over all files to delete each one independently
deleteDataGroup.enter()
Storage.storage().reference().child(userId).listAll { list, error in
if let error = error { print(error) }
list.items.forEach({ file in
self.deleteDataGroup.enter()
file.delete { error in
if let error = error { print(error) }
self.deleteDataGroup.leave()
}
})
deleteDataGroup.leave()
}
}
}
/// Delete user
func deleteUser(user currentUser: User) {
currentUser.delete { error in
if let error = error {
if AuthErrorCode(rawValue: error._code) == .requiresRecentLogin {
reauthenticate()
} else {
// Another error occurred
}
return
}
// Logout properly
try? Auth.auth().signOut()
GIDSignIn.sharedInstance.signOut()
LoginManager().logOut()
// The user has been deleted successfully
// TODO: Redirect to the login UI
}
}
func reauthenticate() {
// TODO: Display some UI to get credential from the user
let credential = ... // Complete from https://stackoverflow.com/a/38253448/8157190
Auth.auth().currentUser?.reauthenticate(with: credential) { _, error in
if let error = error {
print(error)
return
}
// Reload user (to update metadata.lastSignInDate)
Auth.auth().currentUser?.reload { error in
if let error = error {
print(error)
return
}
// TODO: Dismiss UI
// Call `deleteUserProcess()` again, this time it will delete the user
deleteUserProcess()
}
}
}
The minuets function can be added in an extension to Date (thanks to Leo Dabus):
extension Date {
/// Returns the amount of minutes from another date
func minutes(from date: Date) -> Int {
return Calendar.current.dateComponents([.minute], from: date, to: self).minute ?? 0
}
}