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:
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
}
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 theAuthErrorUserInfoUpdatedCredentialKey
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)
}
}
}
}
}