After reading in the Firebase Documentation how to manage account errors with the same email but different credentials I modified my code as the Firebase documentation recommends but I'm still finding problems
When authentication with Facebook encounters the AuthErrorCode.accountExistsWithDifferentCredential.rawValue
error, it obtains a temporary credential (it can occur in (error! As NSError) .userInfo [AuthErrorUserInfoUpdatedCredentialKey]
)
then search within Firebase for the providers that the user has used up to now with the Auth.auth().fetchSignInMethods
When it meets (in my example) the provider "apple.com", the documentation says to authenticate the user and make (if all goes well) the link with the provider using the temporary credential obtained.
Now my problem is that when Auth.auth().fetchSignInMethods meets the provider "apple.com" I can't authenticate with this provider because apple credentials can only be used once ... So at this point I wonder how I get out of this situation?
If we follow the documentation it says to authenticate with EmailAuthProviderID but obviously I think this is just an example .. in the documentation I think that instead of having met (as in my case) the provider "apple.com" met EmailAuthProviderID ...
Am I wrong with this interpretation of the documentation?
Am I making other mistakes and not realizing it?
Can a Facebook account not be linked to an existing Apple account?
Sorry but I have been banging my head on this problem for days ..
This is the updated code
Auth.auth().signIn(with: FacebookAuthProvider.credential(withAccessToken: authToken)) { authResult, error in
if error != nil {
// Handle error.
if (error as NSError?)?.code == AuthErrorCode.accountExistsWithDifferentCredential.rawValue {
// Get pending credential and email of existing account.
let existingAcctEmail = (error! as NSError).userInfo[AuthErrorUserInfoEmailKey] as! String
let pendingCred = (error! as NSError).userInfo[AuthErrorUserInfoUpdatedCredentialKey] as! AuthCredential
Auth.auth().fetchSignInMethods(forEmail: existingAcctEmail) { (methods, error) in
if (methods?.contains("apple.com"))! {
// **** This Flow stops here because I can't reuse Apple's credentials a second time. *****
let tokenID = KeychainManager.getItemFromKeychain(forKey: AuthKey.applTokenID, keyPrefix: AuthKey.prefix)!
let nonce = KeychainManager.getItemFromKeychain(forKey: AuthKey.applNonce, keyPrefix: AuthKey.prefix)
let appleCredentials = OAuthProvider.credential(withProviderID: "apple.com", accessToken: tokenID)
Auth.auth().signIn(with: appleCredentials) { user, error in
if user != nil {
// Link pending credential to account.
Auth.auth().currentUser?.link(with: pendingCred) { result, error in
// ...
print("\n LINK")
}
}
else {
print(error!)
}
}
}
}
}
}
Enable Apple as a sign-in providerAdd Firebase to your Apple project. Be sure to register your app's bundle ID when you set up your app in the Firebase console. In the Firebase console, open the Auth section. On the Sign in method tab, enable the Apple provider.
To send a password reset email to user, on the Users page, hover over the user and click ... > Reset password. The user will receive an email with instructions on how to reset their password. You can customize the email from the Email Templates page.
You create a new user in your Firebase project by calling the createUserWithEmailAndPassword method or by signing in a user for the first time using a federated identity provider, such as Google Sign-In or Facebook Login.
Go to Settings > Passwords. Under Security Recommendations, tap an app or website name. * Tap Use Sign in with Apple, then follow the onscreen steps.
This is the solution that I have adopted .. Obviously it must be customized according to the functioning of your App
So far I have solved asking the user to log in with the existing provider. When the user logs in with the existing provider, he creates the connection with the chosen provider .
I explain ...
If the user wants to log in with Apple and an account with Google already exists, the code should be set in this way to be sure that the user really wants to connect the two providers:
didCompleteWithAuthorization authorization:
(if the user logs in with apple for the first time) check with Auth.auth (). FetchSignInMethods which providers already exist with his email retrieved from his appleID informationNote:
Apple's user data is provided only on its first access. For all other accesses with Sign in With Apple the user data will not be provided
If there are no providers associated with the user's email, complete the authentication flow
2.1. If there are existing providers with the same email (for example GOOGLE), first of all we show a notice that informs the user which providers are associated with his email (previous authentications with other providers) then we allow the user to choose with which provider (in this case GOOGLE) wants to log in to connect Apple.
2.2 When the user chooses the provider (GOOGLE) with which to connect Apple we recall the authentication flow of the existing provider (as if the user was pushing the GOOGLE button).
At this point with the authentication flow included (in our case Google) and if everything is successful, we call Auth.auth (). CurrentUser.link (with: credential)
with Apple credentials
Note:
In all this, Apple's credentials were saved inside the Keychain when the user pushed the button and deleted immediately after the link with the existing provider
Apple credentials cannot be reused once used
So, this is the solution that I've always worked with and it is mentioned in the Firebase
Documentation itself. First of all, you need to understand the concept of linking Multiple Auth Providers
. I'm trying to be as elaborative as possible as a lot of you are facing this issue. Firebase
is owned by Google
and every time you sign in with Google
it automatically overrides the other sign-in methods like Apple Sign In
, Facebook
, Google
.
So, to solve that use the Google Sign In
button. I'm keeping my answer generic even the Javascript
developers can follow this as I've done the same thing in JS
as well.
You have to get the email
from Google
and every provider like FB, Apple
etc. Then you have to use this method: fetchSignInMethods
.
func fetchSignInMethods(provider: String, email: String) {
Auth.auth().fetchSignInMethods(forEmail: email) { (providers, error) in
if let error = error {
print(error)
return
}
let matched = providers?.filter{ $0 == provider }.count //Here I check if the provider I sent example `Facebook` and the fetched provider are same or not, if it is matched then I can go ahead and login the user directly else I have to show them linking alert as already this email exists with different sign in methods
if providers?.isEmpty || matched == 1 {
var credential: AuthCredential!
switch provider {
case Constants.LoginProviders.Google.rawValue: //google.com
credential = GoogleAuthProvider.credential(withIDToken: idToken,accessToken: token) //idToken and AccessToken are fetched from Google Sign In Button
default: //facebook.com
credential = FacebookAuthProvider.credential(withAccessToken: token) //AccessToken is fetched from Facebook Sign In Button
break
}
self.loginWithCredential(credentail: credential, provider: provider)
} //I am also checking if provider is empty then I can sign them up for Email and Password users using Auth.auth().createUserWithEmailAndPassword
else {
self.displayLinkingAlert(provider: fetchedProviderName)
}
}
}
//This function is used to display the alert when an account needs to be linked
//Provider is the parameter which will be fetched from LoginProviders(Constants.swift)
private func displayLinkingAlert(provider: String) {
MKProgress.hide()
let providerName = getProviderName(provider: provider)
let alertC: UIAlertController = UIAlertController(title: "Link Accounts?", message: "This account is already linked with \(providerName). Do you want to link accounts?", preferredStyle: .alert)
let linkAction: UIAlertAction = UIAlertAction(title: "Link account with \(providerName)", style: .default) { (_) in
self.linkAccounts.toggle() //Toggle linkAccounts to true to link the accounts of user
switch provider {
case Constants.LoginProviders.Google.rawValue:
GIDSignIn.sharedInstance()?.signIn()
break
default: self.facebookLoginWithPermissions(from: LoginViewController())
}
}
let cancelAction: UIAlertAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
let actions: [UIAlertAction] = [linkAction, cancelAction]
actions.forEach{ alertC.addAction($0) }
UIApplication.topViewController()?.present(alertC, animated: true, completion: nil)
} //I also too a private var linkAccounts = false above in the class
//This function is used to get the providerName from the provider
//Ex.:- provider -> google.com, providerName: Google
private func getProviderName(provider: String) -> String
{
var providerName = ""
switch provider
{
case Constants.LoginProviders.Email.rawValue : providerName = "Email"
case Constants.LoginProviders.Facebook.rawValue : providerName = "Facebook"
case Constants.LoginProviders.Google.rawValue : providerName = "Google"
default: break
}
return providerName
}
Now, this will show you the linking alert. Well, it is up to you how you want to proceed. In my apps, I've Google
, Facebook
and Email
sign in methods enabled and I show the users both of them in case if two providers are fetched from Firebase
. For eg.
If I have already signed up with Google
and Facebook
, then if I try to sign the user up with email it will show me two alert buttons Link With Google
and Link With Facebook
. Now, it is up to you if you want you can show them both or you can just them one, I can even add Apple Sign In
here, it's up to individual's choice.
Now, when the alert appears and you press the Link With Facebook
option firstly it will toggle
the linkAccount
variable to true and then, it will call the facebookTap
method which will again fetch
the providers
like this:
private func facebookTap() {
//Get the details and get the email then...
fetchSignInMethods(provider: Constants.LoginProviders.Facebook.rawValue //facebook.com, email: emailFetched) //This will sign in the user using Facebook and as the linkAccount variable is true it will link ther user
}
//A common function used to login the users, by using their credentails
//Parameter credential is of type AuthCredential which is used to
//signInAndRetrieve data of a particular user.
func loginWithCredential(credentail: AuthCredential, provider: String) {
authInstance.signInAndRetrieveData(with: credentail) { [unowned self](authResult, error) in
if let error = error {
print(error)
} else {
if self.linkAccounts {
var loginCredential: AuthCredential!
switch provider {
case Constants.LoginProviders.Google.rawValue:
guard let facebookTokenString = AccessToken.current?.tokenString else { return }
loginCredential = FacebookAuthProvider.credential(withAccessToken: facebookTokenString)
break
default:
loginCredential = GoogleAuthProvider.credential(withIDToken: self.googleIdToken, accessToken: self.googleAccessToken)
break
}
self.linkAccounts(credential: loginCredential)
} else {
self.getFirebaseToken()
}
}
}
}
Now, I am using this to link
accounts:
//This function is used to link the Accounts
//Ex:- Google with Facebook, vice-versa
private func linkAccounts(credential: AuthCredential) {
authInstance.currentUser?.link(with: credential, completion: { (authResult, error) in
if let error = error {
print(error)
return
} else {
self.linkAccounts.toggle() //Toggle Link Account to false as acccounts are already linked
//Navigate user to another page or do your stuff
}
})
}
Now, this is the solution that I tried and it worked for me everytime. It worked for me in Swift
, Flutter
, Javascript
and it will work with every other language. You can do the same thing with Apple Sign In Button
. In the delegate where you get the details if user shared their email address then you can use fetchSignInMethods
and it will automatically show with the above alert. I've not got the chance to link the user who chose to keep their identity secret, but I will update my answer with that very soon.
I've tried to be as elaborative as possible, please do let me know if you have any queries.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With