If a user is originally authenticated with a Google Provider and I allow the user to linkWithCredential via the password Provider, it seems to update my primary email address in Firebase auth, even without email verification.
This is not desired, the original Google Provider email address should be maintained.
I'm using Vue3, Pinia and of course Firebase auth to handle this but I can't seem to figure out a way around Firebase's updateEmail method automatically doing this for me. Somehow it seems to bypass Firebase's security measures, or may be working as intended??
Even if I unlink the password Provider (which is now the primary email in Firebase auth) it still seems to keep that as the primary email, when in fact I have logic to revert to Google Provider email.
Essentially my goal is to allow a user to link a new provider to their existing account, without updating the primary email address. Offer a way to unlink either the Google Provider or Password Provider while gracefully reverting back to the prior method or alternative method (reauthenticating here is fine too).
Any advice tips based on my crude code snippets? (not full code for both files)
Vue component:
// Authentication (Email / Google Providers)
const authFirebase = getAuth();
const showEmailChangeFields = ref(false);
const password = ref('');
const googleSignInLogo = ref(googleLogo);
const canResendEmail = ref(true);
const connectWithGoogle = async () => {
try {
const result = await authStore.linkGoogleAccount();
if (result.success) {
successMessage.value = result.message;
} else {
error.value = result.message;
}
} catch (err) {
console.error('Error connecting with Google:', err);
error.value = err.message || 'Failed to connect Google account. Please try again.';
}
};
const hasEmailProvider = computed(() => {
return authStore.user.providerData.some((data) => data.providerId === 'password');
});
const hasGoogleProvider = computed(() => {
return authStore.user.providerData.some((data) => data.providerId === 'google.com');
});
const showConnectGoogleOption = computed(() => {
return !hasGoogleProvider.value && hasEmailProvider.value;
});
const showAddEmailOption = computed(() => {
return hasGoogleProvider.value && !hasEmailProvider.value;
});
const resendVerificationEmail = async () => {
console.log('Attempting to resend verification email');
if (!canResendEmail.value) {
console.log('Resending too soon');
return;
}
canResendEmail.value = false;
try {
await authStore.sendEmailVerification();
console.log('Verification email sent successfully');
successMessage.value = 'Verification email sent. Please check your inbox.';
setTimeout(() => {
canResendEmail.value = true;
}, 60000);
} catch (error) {
console.error('Error sending verification email:', error);
error.value = 'Failed to send verification email. Please try again.';
canResendEmail.value = true;
}
};
const sendVerificationForEmailAddition = async () => {
console.log('Attempting to add email:', newEmail.value);
if (!newEmail.value || !password.value) {
console.log('Missing email or password');
error.value = 'Both new email and password must be provided.';
return;
}
if (!canResendEmail.value) {
console.log('Resending too soon');
error.value = 'Please wait before resending the verification email.';
return;
}
try {
// This method should only link the email/password without updating the primary email.
const result = await authStore.linkEmailAndPassword(newEmail.value, password.value);
console.log('linkEmailAndPassword result:', result);
if (result.success) {
await authStore.refreshUser();
successMessage.value = 'Verification email sent. Please check your inbox.';
} else {
error.value = result.message;
}
// Reset UI state
newEmail.value = '';
password.value = '';
showEmailChangeFields.value = false;
canResendEmail.value = false;
setTimeout(() => {
canResendEmail.value = true;
}, 60000);
} catch (err) {
console.error('Error during email addition:', err);
error.value = err.message || 'Failed to add email. Please try again.';
}
};
const makeEmailPrimary = async (email) => {
console.log('Attempting to set primary email:', email);
try {
if (authStore.user.email === email) {
throw new Error('This email is already set as primary.');
}
// If the new primary email is from Google, re-authenticate the user first.
if (email === authStore.user.providerData.find(pd => pd.providerId === 'google.com')?.email) {
// Trigger re-authentication flow here.
await authStore.reauthenticateGoogle();
}
// After successful re-authentication, or if no re-authentication was necessary, update the primary email.
const updateResult = await authStore.makeEmailPrimary(email);
if (updateResult.success) {
successMessage.value = 'Primary email updated successfully.';
// Optionally log the updated primary email for confirmation.
authStore.logPrimaryEmail();
} else {
throw new Error(updateResult.message);
}
} catch (error) {
console.error('Error during email primary update:', error);
return { success: false, message: `Failed to update primary email: ${error.message}` };
}
};
const unlinkProvider = async (providerId) => {
console.log('Attempting to unlink provider:', providerId);
try {
if (authStore.user.providerData.length > 1) {
await unlink(authFirebase.currentUser, providerId);
console.log(`Provider ${providerId} unlinked successfully`);
if (providerId === 'password') {
const googleProviderData = authStore.user.providerData.find(pd => pd.providerId === 'google.com');
if (googleProviderData && googleProviderData.email) {
try {
// Attempt to update the email back to the Google one.
await updateEmail(authFirebase.currentUser, googleProviderData.email);
console.log('Firebase Auth email reverted to Google email:', googleProviderData.email);
await authStore.updateUserInfo({ email: googleProviderData.email });
} catch (error) {
if (error.code === 'auth/requires-recent-login') {
console.log('User needs to re-authenticate to update their email.');
} else {
console.error('Error updating email:', error);
}
}
} else {
console.log('Google email not found or user not signed in with Google.');
}
}
successMessage.value = `Successfully unlinked ${getProviderName(providerId)}`;
await authStore.refreshUser(); // Refresh local user data to reflect changes
} else {
console.log('Cannot unlink the last remaining provider');
error.value = 'You cannot unlink the last remaining provider.';
}
} catch (err) {
console.error(`Error unlinking ${getProviderName(providerId)}:`, err);
error.value = `Failed to unlink ${getProviderName(providerId)}. Please try again.`;
}
};
const getProviderName = (providerId) => {
switch (providerId) {
case 'password':
return 'Email';
case 'google.com':
return 'Google';
// Add more cases as needed
default:
return providerId; // Or format as you like
}
};
Pinia Store
// stores/authStore.js
import { defineStore } from 'pinia'
import { auth, storage, firestore } from '@/firebaseConfig'
import {
signInWithEmailAndPassword,
GoogleAuthProvider,
linkWithPopup,
getAuth,
updateProfile,
sendEmailVerification,
signOut,
onAuthStateChanged,
updateEmail,
EmailAuthProvider,
linkWithCredential,
signInWithRedirect,
signInWithPopup,
getRedirectResult
} from 'firebase/auth'
import { doc, setDoc, getDoc, updateDoc } from 'firebase/firestore'
import { useSlidesStore } from '@/stores/slidesStore'
import { ref as storageRef, getDownloadURL, uploadBytes, deleteObject } from 'firebase/storage'
import defaultAvatar from '@/assets/[email protected]'
import { createAvatar } from '@dicebear/core'
import * as styles from '@dicebear/collection'
export const useAuthStore = defineStore('auth', {
state: () => ({
initialAuthCompleted: false,
loading: true,
user: null,
isAuthenticated: false,
defaultPhotoURL: defaultAvatar,
photoStoragePath: null,
fullName: null,
url: null,
organization: null,
location: null,
email: null,
displayName: null
}),
actions: {
// Update user state and authentication status
setUser(user) {
this.user = user
this.isAuthenticated = !!user // Convert the `user` to a boolean to represent authentication status
},
setEmailVerified() {
if (this.user) {
this.user.emailVerified = true
}
},
logPrimaryEmail() {
const currentUser = getAuth().currentUser
if (currentUser) {
console.log('Primary email:', currentUser.email)
} else {
console.log('No user currently signed in.')
}
},
// other code
async linkEmailAndPassword(newEmail, newPassword) {
const currentUser = getAuth().currentUser
if (currentUser) {
try {
console.log('Linking new email:', newEmail)
const credential = EmailAuthProvider.credential(newEmail, newPassword)
await linkWithCredential(currentUser, credential)
console.log('Email linked successfully, sending verification email.')
// Now, send a verification email
await sendEmailVerification(currentUser)
// Update the user's email in Firestore
const userDocRef = doc(firestore, 'users', currentUser.uid)
await updateDoc(userDocRef, {
email: newEmail, // Update Firestore with the new email
emailVerified: false // Set emailVerified to false until the user verifies it
})
// Refresh Pinia authStore state
this.setUser({ ...this.user, email: newEmail, emailVerified: false })
return {
success: true,
message: 'New email linked successfully. Please verify your email.'
}
} catch (error) {
console.error('Error linking new email:', error)
throw error
}
} else {
throw new Error('No user currently signed in.')
}
},
async makeEmailPrimary(newEmail) {
const currentUser = getAuth().currentUser
if (currentUser && newEmail !== currentUser.email) {
try {
// The reauthentication should have just occurred before this action is triggered.
await updateEmail(currentUser, newEmail)
// Update Firestore and local authStore only after successful email update.
const userDocRef = doc(firestore, 'users', currentUser.uid)
await updateDoc(userDocRef, { email: newEmail })
this.setUser({ ...this.user, email: newEmail }) // Update the local state.
return { success: true, message: 'Primary email updated successfully.' }
} catch (error) {
console.error('Error updating primary email:', error)
return { success: false, message: `Failed to update primary email: ${error.message}` }
}
} else {
return {
success: false,
message: 'New email is the same as the current one or no user signed in.'
}
}
},
}
})
// Reactive Firebase auth state listener
onAuthStateChanged(auth, (user) => {
const authStore = useAuthStore()
authStore.setUser(user) // Update the store with the current user
})