I'm using firebase auth to sign in users using Facebook, Google and Email/Pass methods. For the federated providers (Google and Facebook), I'm using the signInWithRedirect method.
Currently, a user can sign in using one of these methods without issue.
The problem occurs when a user previously authenticated with one provider, comes back later and choose to sign-in using another provider. Then I get the famous AuthError below:
auth/account-exists-with-different-credential
My understanding is that I must link the accounts.
After a few attempts, I've found this answer about the same issue, but it's implemented using Firebase auth v8, not v9 so it works differently (no credential property in the error object for example).
So, I understand that once a user successfully signs in, I must store it as a User object with the associated OAuthCredential in the sessionStorage. What must happens next, I don't get it ...
Here's how the code looks like up to now ...
Login part
export function loginWithGoogle() {
try {
let provider = new GoogleAuthProvider();
provider.addScope("email");
signInWithRedirect(auth, provider);
} catch(e){
// ...
}
}
export function loginWithFacebook() {
try {
let provider = new FacebookAuthProvider();
provider.addScope("email");
signInWithRedirect(auth, provider);
} catch(e){
// ...
}
}
Handling Redirection Part
import { credStore } from "$lib/stores/cred.js";
let cred; // cred holds the credStore value
$: if (credStore) {
credStore.subscribe((value) => {
cred = value;
});
}
let unsubAuthObserver;
export const auth = getFirebaseAuth();
function getFirebaseAuth() {
let auth = getAuth(getFirebaseApp());
auth.useDeviceLanguage(); // use the default browser language
// see https://firebase.google.com/docs/reference/js/auth.md#onauthstatechanged
unsubAuthObserver = onAuthStateChanged(auth, (authUser) =>
handleRedirect(authUser)
);
return auth;
}
async function handleRedirect(authUser) {
try {
const rResult = await getRedirectResult(auth); // see https://firebase.google.com/docs/reference/js/auth.usercredential
if (rResult && rResult?.user) {
// user signed-in
const cred = OAuthProvider.credentialFromResult(rResult);
// save the user and credential in a sessionStorage store
credStore.set({ cred: cred, user: auth.currentUser });
// get info from DB for that user
getUserFromDB(rResult?.user, redir);
}
} catch (err) {
// user tried to sign in using a different method
if (err.code === "auth/account-exists-with-different-credential") {
// if something is in the cred store
if (cred?.cred && cred?.user) {
// HERE I'M LOST
// tried to sign in the user with the stored cred from the previous successful attempt
const result = await signInWithCredential(auth, cred.cred);
// ˆ ERROR: credential._getIdTokenResponse is not a function
// get the credential used with the new Provider
// const credential = OAuthProvider.credentialFromResult(result);
// console.log(credential);
// const linkResult = await linkWithCredential(cred.user, credential);
// console.log(linkResult);
// Sign in with the newly linked credential
// const linkCredential = OAuthProvider.credentialFromResult(linkResult);
// const signInResult = signInWithCredential(auth, linkCredential);
// console.log(signInResult);
// console.log(signInResult.user);
// TODO: Save the merged data to the new user
} else console.error(err);
}
}
[...]
export async function signOutOfFirebase() {
try {
unsubAuthObserver();
await signOut(auth);
} catch (error) {
console.error(error);
}
}
Thank you so much
Edit 20/12/2022
Just saw that there's a sveltekit-auth out there. I'll give it a try so see if I can manage user auth without Firebase. Taking a step back, Firebase auth is the only service I use from Firebase, so maybe I can manage these cases when a user authenticate using different providers by myself. I'll comment in a few days here.
Edit 27/12/2022
For those interested here's a simple implementation with Google and Facebook sveltekit-auth. You'll see that there's an account linking option called allowDangerousEmailAccountLinking. It can freak you out but it all depends on the providers you're using. The problem resides in the case when an attacker creates a social account tied to an email that it doesn't own. With account linking, he would be granted the same access as the email victim. That's why account linking is not an option enabled by default. https://next-auth.js.org/configuration/providers/oauth#allowdangerousemailaccountlinking-option. sveltekit-auth is still experimental, this means that I can't use it right now. In other words, if I want to get rid of Firebase, I would need to manually implement FB, GG and login/pass auth by myself ...