Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Reuse Credential for Sign in with Apple in Firebase iOS

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!)

                        }
                    }
                }
            }
        }
    }
like image 830
kAiN Avatar asked Mar 13 '20 15:03

kAiN


People also ask

How do I set up login with Apple Firebase?

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.

How do I reset my Firebase authentication password?

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.

How do I change my Firebase authentication username?

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.

How do I enable Apple sign in?

Go to Settings > Passwords. Under Security Recommendations, tap an app or website name. * Tap Use Sign in with Apple, then follow the onscreen steps.


2 Answers

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:

  1. in the delegated method of AutenticationService 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 information

Note:

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


  1. 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


like image 94
kAiN Avatar answered Oct 18 '22 19:10

kAiN


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.

Once Email, Facebook and Google are linked

like image 23
Rob Avatar answered Oct 18 '22 21:10

Rob