Hi guys, I'm migrating from firebase web sdk to this library. On the web sdk, when calling signInWithCredential if the account is being used with another provider the sdk throws an error with code auth/account-exists-with-different-credential and the contents are code, credential and email. If one follows the steps at https://firebase.google.com/docs/auth/web/facebook-login you can find out the provider used for this account and link them. I don't seem to be able to get the same information using your library. All I can see inside the error object is framesToPop and code.
Any ideas?
I do remember something about this when I wrote the new auth errors, couldnt find this functionality on the firebase native sdks, https://firebase.google.com/docs/reference/android/com/google/firebase/auth/FirebaseAuthUserCollisionException is the android counterpart to the error you mentioned above, but theres no way to get the credential from it. However there is fetchProvidersForEmail which if you give it an email addresa it will give you back an array of provider strings - if that helps?
Yeah that's the one I was using on the web sdk but if the user performs a
Facebook or Google login you don't get the email as this is handled on
their modals etc that's why I guess it's returned as part of the error on
the web sdk. I obviously want to avoid asking the user for his email if the
login fails just so I can link it with the right provider. I guess this is
another inconsistency across Firebase SDKs :(
On Wed, 6 Sep 2017, 00:49 Michael Diarmid notifications@github.com wrote:
I do remember something about this when I wrote the new auth errors,
couldnt find this functionality on the firebase native sdks,
https://firebase.google.com/docs/reference/android/com/google/firebase/auth/FirebaseAuthUserCollisionException
is the android counterpart to the error you mentioned above, but theres no
way to get the credential from it. However there is fetchProvidersForEmail
which if you give it an email addresa it will give you back an array of
provider strings - if that helps?—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
https://github.com/invertase/react-native-firebase/issues/389#issuecomment-327334513,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAL9sibQor7Yz-x3-3TghaXkRwUzoQlRks5sfd4AgaJpZM4PNkKN
.
You can get emails from both of those though, you just need to request the correct scope, https://developers.facebook.com/docs/facebook-login/permissions/#reference-email for example
And ye there's quite a few inconsistency issues on the firebase sdks annoyingly, best to flag them with firebase directly, its what I do ha
Yeah, you are right. I spent yesterday so much time around these things that I forgot the basics :)
Thanks!
No worries, good luck!
Do you have an example of how to handle this duplicate credentials issue? I'm having a hard time figuring out how to handle that. All the examples I see are using signInWithPopup() which isn't implemented on RN. If, let's say I first signed in using Google, and now trying via Facebook, it'll throw this error and I need to somehow get the Google credentials - but I'm not sure how to get those. Any help?
Here is what my facebook sign in block looks like, you can follow through it (or maybe you can, it's way too long, and kind of ridiculous, sorry) to see how I handle it
async facebookSignIn(link?: boolean): Promise<boolean> {
try {
const result = await LoginManager.logInWithPermissions(['public_profile', 'email']);
if (result.isCancelled) {
// handle this however suites the flow of your app
console.log('UserStore::facebookSignIn - User cancelled request');
return Promise.resolve(false);
}
console.log('UserStore::facebookSignIn success, result:', JSON.stringify(result, null, 2));
// Get the email
const facebookProfile = await this.getEmailForFacebookLogin();
// @ts-ignore
const providers = await this.getProvidersForEmail(facebookProfile.email);
// if facebook.com is not in the providers, it is first login via facebook.
let facebookProvider = false;
let passwordProvider = false;
for (let i = 0; i < providers.length; i++) {
if (providers[i] === 'facebook.com') {
facebookProvider = true;
} else if (providers[i] === 'password') {
passwordProvider = true;
}
}
// If there is password provider, but not facebook, google actually just automatically does it right.
if (providers.length !== 0 && !facebookProvider) {
if (passwordProvider) {
console.log(
'UserStore::facebookSignIn - other providers but google auto-connects if it is password provider'
);
} else {
this.handleCredentialInUse();
return Promise.resolve(false);
}
}
// If there are no other providers we should ask if they already have an account
if (
providers.length === 0 &&
(!firebase.auth().currentUser?.providerData ||
firebase.auth().currentUser?.providerData.length === 0)
) {
console.log('UserStore::facebookSignIn - no other providers. See if they are sure');
const choice = await AlertAsync(
I18NService.translate('LoginFirstLoginTitle'),
I18NService.translate('LoginFirstLoginText'),
[
{
text: I18NService.translate('LoginFirstLoginOtherAccountsButton'),
onPress: () => 'Other Accounts',
},
{
text: I18NService.translate('LoginFirstLoginPleaseContinueButton'),
onPress: () => 'Please Continue',
},
],
{
cancelable: false,
onDismiss: () => 'Please Continue',
}
);
if (choice === 'Other Accounts') {
console.log('UserStore::facebookSignIn - user wants to handle other accounts');
return Promise.resolve(false);
}
console.log(
'UserStore::facebookSignIn - user is just fine continuing and creating new account'
);
this.setIsNewUser(true);
}
// get the access token
const facebookToken = await AccessToken.getCurrentAccessToken();
console.log(
'UserStore::facebookSignIn access token:',
JSON.stringify(facebookToken, null, 2)
);
if (!facebookToken) {
// handle this however suites the flow of your app
console.log('UserStore::facebookSignIn - error obtaining the users access token');
return Promise.resolve(false);
}
// create a new firebase credential with the token
this.removeUserChangeListener();
const credential = firebase.auth.FacebookAuthProvider.credential(facebookToken.accessToken);
console.log('UserStore::facebookSignIn - credential is', JSON.stringify(credential, null, 2));
let firebaseUserCredential;
if (!link) {
// login with credential
// If a firebase auth entry with that email already exists, we get this:
// Error: An account already exists with the same email address but different sign-in credentials. Sign in using a provider associated with this email address.
// Direct them to login and connect accounts at that point?
firebaseUserCredential = await firebase.auth().signInWithCredential(credential);
Analytics.setAnalyticsUser(firebaseUserCredential.user.uid); // TODO, set our own ID?
Analytics.setAnalyticsUserProperties({ email: firebaseUserCredential.user.email });
Analytics.analyticsEvent('successFacebookSignIn');
} else {
if (!firebase.auth().currentUser) {
return Promise.resolve(false);
}
firebaseUserCredential = await firebase.auth().currentUser!.linkWithCredential(credential);
Analytics.setAnalyticsUser(firebase.auth().currentUser!.uid); // TODO, set our own ID?
Analytics.setAnalyticsUserProperties({ email: firebase.auth().currentUser!.email });
Analytics.analyticsEvent('successFacebookLink');
}
console.log(
'UserStore::facebookSignIn - firebaseUserCredential is',
JSON.stringify(firebaseUserCredential, null, 2)
);
this.userChangedHandler(
firebaseUserCredential!.user,
firebaseUserCredential!.additionalUserInfo
);
return Promise.resolve(true);
} catch (e) {
console.log('UserStore::facebookSignIn - error?', e);
if (e.code === 'auth/email-already-in-use' || e.code === 'auth/credential-already-in-use') {
console.log('UserStore::_googleSignIn - email already in use, instruct on unlink/delete');
this.handleAccountInUse();
} else if (e.code === 'auth/account-exists-with-different-credential') {
this.handleCredentialInUse();
} else {
RX.Alert.show(
I18NService.translate('ConnectedAccountsConnectionError'),
I18NService.translate(e.code)
);
}
} finally {
this.addUserChangedListener();
}
return Promise.resolve(false);
}
@mikehardy What I'm actually looking for is your implementation of handleCredentialInUse() . ie if Firebase is already tied to Google credentials, how do I tie the Facebook account to the existing Firebase user?
it's in there, about a thousand lines down :sweat_smile:
} else {
if (!firebase.auth().currentUser) {
return Promise.resolve(false);
}
firebaseUserCredential = await firebase.auth().currentUser!.linkWithCredential(credential);
Analytics.setAnalyticsUser(firebase.auth().currentUser!.uid); // TODO, set our own ID?
Analytics.setAnalyticsUserProperties({ email: firebase.auth().currentUser!.email });
Analytics.analyticsEvent('successFacebookLink');
}
the key is that I split it into two branches: one where I'm directing this function to link, one where I'm not. If I determine the credential is in use and I have not explicitly said "link it" to the function, then I blow out and direct the user to login to the other account and either delete it, or link this social account to that already existing account.
If I have said to link, then I link this social account credential to the current account.
Now if they go to the other account (by logging out, and logging in with the other account) they can go to a "connection management" screen where they can then try to link the other social account to this one.
I do that for all the social auth providers, so I have a panel with them all listed, current connection status, and a connect/disconnect button
The problem I have is firebase.auth().currentUser is null. The user has not signed in recently via the other provider. So how can I successfully call:
firebaseUserCredential = await firebase.auth().currentUser!.linkWithCredential(credential);
Is it possible to get the credentials from the existing tied account (ie Google), so I can call:
firebase.auth().signInWithCredential(googleCredentials)
I don't know. I don't let users use my app when they aren't logged in.
@mikehardy I don't understand. I'm getting this error (I'm assuming) because the user (once upon a time) signed in to my app using Google Sign-in, then signed out, then tried signing in using Facebook. Firebase throws this error because the account already exists, tied to the Google credentials. How can I merge the facebook credentials to the existing firebase account?
I don't understand your comment about "I don't let users use my app when they aren't logged in" - if you're facing an error regarding a pre-existing firebase account (ie signed in via email, then later via facebook), why is your code assuming they're already signed in, in order to link the facebook account?
What does your handleCredentialInUse() look like?
Is there a better way to handle this than telling them "Please sign in using X method"? Or to switch over to the sign-in process they used before (ie if Firebase is linked to Google sign-in)?
Basically I'm trying to do this code below, but in React Native (since signInWithPopup isn't implemented). I'm assuming if the account exists with another provider that it kicks them over to that provider's login flow?
https://stackoverflow.com/a/55581920/1104794
function getProvider(providerId) {
switch (providerId) {
case firebase.auth.GoogleAuthProvider.PROVIDER_ID:
return new firebase.auth.GoogleAuthProvider();
case firebase.auth.FacebookAuthProvider.PROVIDER_ID:
return new firebase.auth.FacebookAuthProvider();
case firebase.auth.GithubAuthProvider.PROVIDER_ID:
return new firebase.auth.GithubAuthProvider();
default:
throw new Error(`No provider implemented for ${providerId}`);
}
}
const supportedPopupSignInMethods = [
firebase.auth.GoogleAuthProvider.PROVIDER_ID,
firebase.auth.FacebookAuthProvider.PROVIDER_ID,
firebase.auth.GithubAuthProvider.PROVIDER_ID,
];
async function oauthLogin(provider) {
try {
await firebase.auth().signInWithPopup(provider);
} catch (err) {
if (err.email && err.credential && err.code === 'auth/account-exists-with-different-credential') {
const providers = await firebase.auth().fetchSignInMethodsForEmail(err.email)
const firstPopupProviderMethod = providers.find(p => supportedPopupSignInMethods.includes(p));
// Test: Could this happen with email link then trying social provider?
if (!firstPopupProviderMethod) {
throw new Error(`Your account is linked to a provider that isn't supported.`);
}
const linkedProvider = getProvider(firstPopupProviderMethod);
linkedProvider.setCustomParameters({ login_hint: err.email });
const result = await firebase.auth().signInWithPopup(linkedProvider);
result.user.linkWithCredential(err.credential);
}
// Handle errors...
// toast.error(err.message || err.toString());
}
}
Is there a better way to handle this than telling them "Please sign in using X method"? Or to switch over to the sign-in process they used before (ie if Firebase is linked to Google sign-in)?
Not that I know of
OK this is the code I came up with (accepting Apple, Facebook and Google sign-in), for anyone's reference. It seems to only fail/warn about an existing account on the first attempt. After proper linking, it'll work as normal:
import auth, { firebase } from '@react-native-firebase/auth'
import { AccessToken, LoginManager, GraphRequest,
GraphRequestManager } from 'react-native-fbsdk'
import { GoogleSignin } from '@react-native-community/google-signin'
// ... Inside component:
async appleLogin () {
const appleAuthRequestResponse = await appleAuth.performRequest({
requestedOperation: AppleAuthRequestOperation.LOGIN,
requestedScopes: [AppleAuthRequestScope.EMAIL, AppleAuthRequestScope.FULL_NAME]
})
const { identityToken, fullName, email, nonce } = appleAuthRequestResponse
const displayName = fullName.givenName + ' ' + fullName.familyName
if (identityToken) {
const appleCredential = firebase.auth.AppleAuthProvider.credential(identityToken, nonce)
this.signInWithCredential(appleCredential, email, displayName)
} else {
Alert.alert('Apple Sign-In Error', 'Unable to sign-in')
}
}
async getFacebookProfile () {
return new Promise(resolve => {
const infoRequest = new GraphRequest(
'/me?fields=email,name',
null,
(error, result) => {
if (error) {
console.log('Error fetching data: ' + error.toString())
resolve(null)
return
}
resolve(result)
}
)
new GraphRequestManager().addRequest(infoRequest).start()
})
}
async facebookLogin () {
let credential = null
try {
const result = await LoginManager.logInWithPermissions(['public_profile', 'email'])
if (result.isCancelled) {
// handle this however suites the flow of your app
throw new Error('User cancelled request')
}
const data = await AccessToken.getCurrentAccessToken()
if (!data) {
throw new Error('Something went wrong obtaining the users access token')
}
// create a new firebase credential with the token
credential = firebase.auth.FacebookAuthProvider.credential(data.accessToken)
const profile = await this.getFacebookProfile()
const { email, name } = profile
this.signInWithCredential(credential, email, name)
} catch (error) {
console.log(error)
}
}
async signInWithCredential (credential, email, displayName) {
// login with credential
firebase.auth().signInWithCredential(credential).then(firebaseUserCredential => {
if (this.state.linkedCredential) {
firebaseUserCredential.user.linkWithCredential(this.state.linkedCredential)
this.setState({ linkedCredential: null })
}
if (displayName) {
firebaseUserCredential.user.updateProfile({ displayName })
}
}).catch(async (error) => {
if (error.code === 'auth/account-exists-with-different-credential') {
if (email) {
firebase.auth().fetchSignInMethodsForEmail(email).then(providers => {
console.log('PROVIDERS=', providers)
this.setState({ linkedCredential: credential }, () => {
if (providers.includes('apple.com') && appleAuth.isSupported) {
Alert.alert('Sign-in via Apple', "Looks like you previously signed in via Apple. You'll need to sign-in there to continue", [
{ text: 'Cancel', style: 'cancel' },
{ text: 'Continue', onPress: () => this.appleLogin() }
])
} else if (providers.includes('google.com')) {
Alert.alert('Sign-in via Google', "Looks like you previously signed in via Google. You'll need to sign-in there to continue", [
{ text: 'Cancel', style: 'cancel' },
{ text: 'Continue', onPress: () => this.googleLogin() }
])
} else if (providers.includes('facebook.com')) {
Alert.alert('Sign-in via Facebook', "Looks like you previously signed in via Facebook. You'll need to sign-in there to continue", [
{ text: 'Cancel', style: 'cancel' },
{ text: 'Continue', onPress: () => this.facebookLogin() }
])
} else {
Alert.alert('Login Error', 'Sign in using a different provider')
}
})
})
} else {
Alert.alert('Login Error', 'Unable to sign in using account, could not determine email')
}
} else {
Alert.alert('Login Error', error.toString())
}
})
}
async googleLogin () {
try {
// add any configuration settings here:
await GoogleSignin.configure()
GoogleSignin.signIn().then(data => {
console.log('GOOGLE DATA=', data)
// create a new firebase credential with the token
const credential = firebase.auth.GoogleAuthProvider.credential(data.idToken, data.accessToken)
// login with credential
this.signInWithCredential(credential, data.user.email, data.user.name)
}).catch(error => {
console.log('GOOGLE ERROR=', error)
})
} catch (e) {
console.error(e)
}
}
Most helpful comment
OK this is the code I came up with (accepting Apple, Facebook and Google sign-in), for anyone's reference. It seems to only fail/warn about an existing account on the first attempt. After proper linking, it'll work as normal: