iosswiftfirebasefirebase-authentication

Firebase Auth - Link Anonymous user to Apple


I struggle understanding how linking should exactly work from anonymous user to an authenticated user with Apple Sign In. By default, I perform a signInAnonymously when my app starts (if nobody is logged in yet), so there is always a user - either anonymous or not.

Then, the user can initiate the sign in process. I simply check if the user is anonymous and if so I link it. This part works fine. But, when I log out or uninstall/reinstall the app, I end up as an anonymous user. On the next try when I try to sign in, it throws the error that it cannot link against another anonymous user - which is fine and I don't want that - so I call signIn then - but that fails: Duplicate credential received. Please try again with a new credential.

So, what I want:

  1. users are automatically anonymous users so they can use my app
  2. when the user signs in with Apple, the first time I want to link the anonymous account they use
  3. any subsequent time I simply want them to get logged in - I don't care about that anonymous account which is in use.

What am I missing?

This is what I have:

func handleSignInWithAppleCompletion(_ result: Result<ASAuthorization, Error>) {
    if case .success(let success) = result {
        if let appleIDCredential = success.credential as? ASAuthorizationAppleIDCredential {
            let idTokenString = String(data: appleIDCredential.identityToken, encoding: .utf8)
            let credential = OAuthProvider.credential(
                providerID: .apple,
                idToken: idTokenString,
                rawNonce: nonce
            )

            Task {
                if let user = Auth.auth().currentUser, user.isAnonymous {
                    do {
                        _ = try await user.link(with: credential)
                    } catch let linkError as NSError {
                        if linkError.code == AuthErrorCode.credentialAlreadyInUse.rawValue {
                            _ = try await Auth.auth().signIn(with: credential)
                        }
                    }
                } else {
                    _ = try await Auth.auth().signIn(with: credential)

                }
            }
        }
    }
}

This is in the init of my Repository class which acts as a singleton and is created on app start:

init() {
    _ = Auth.auth().addStateDidChangeListener { [weak self] _, user in
        guard let self else { return }
        loginState = loginState(with: user)
        if loginState == .signedOut {
            signInAnonymously() // Simply signing in via Firestore as anonymous user and then setting up listeners
        } else {
            setupListeners() // Setting up Firestore listeners
        }
    }
}

private func loginState(with user: FirebaseAuth.User?) -> LoginState {
    guard let user else {
        return .signedOut
    }
    let loginState: LoginState
    if user.isAnonymous {
        loginState = .anonymous
    } else {
        switch user.providerData.first?.providerID {
        case "apple.com":
            loginState = .signedIn(type: .apple)
        default:
            loginState = .signedIn(type: .unknown)
        }
    }
    return loginState
}

Solution

  • The missing part was stated in the documentation which I overlooked nicely:

    Sign in with Apple will not allow you to reuse an auth credential to link to an existing account. If you want to link a Sign in with Apple credential to another account, you must first attempt to link the accounts using the old Sign in with Apple credential and then examine the error returned to find a new credential. The new credential will be located in the error's userInfo dictionary and can be accessed via the AuthErrorUserInfoUpdatedCredentialKey key.

    Therefore I had to update my code to use the new credential, like this:

    func handleSignInWithAppleCompletion(_ result: Result<ASAuthorization, Error>) {
        if case .success(let success) = result {
            if let appleIDCredential = success.credential as? ASAuthorizationAppleIDCredential {
                let idTokenString = String(data: appleIDCredential.identityToken, encoding: .utf8)
                let credential = OAuthProvider.credential(
                    providerID: .apple,
                    idToken: idTokenString,
                    rawNonce: nonce
                )
    
                Task {
                    if let user = Auth.auth().currentUser, user.isAnonymous {
                        do {
                            _ = try await user.link(with: credential)
                        } catch let linkError as NSError {
                            if linkError.code == AuthErrorCode.credentialAlreadyInUse.rawValue {
                                if let newCredential = linkError.userInfo[AuthErrorUserInfoUpdatedCredentialKey] as? OAuthCredential {
                                    _ = try await Auth.auth().signIn(with: newCredential)
                                }
                            }
                        }
                    } else {
                        _ = try await Auth.auth().signIn(with: credential)
    
                    }
                }
            }
        }
    }