Firebase when is onAuthStateChanged triggered? - firebase

If I have my createUserWithEmailAndPassword function structured like this
await auth.createUserWithEmailAndPassword(email, password).then((userCredential) => {
code stuff...
return promise
}).then(() => {
...
}).catch(() => {
...
}).finally(() => {
...
})
When in the promise chain would the onAuthStateChanged observer fire-up?

The onAuthStateChanged observer is triggered only when a user either signs in or signs out.
From the documentation:
Prior to 4.0.0, this triggered the observer when users were signed in, signed out, or when the user's ID token changed in situations such as token expiry or password change. After 4.0.0, the observer is only triggered on sign-in or sign-out.
To keep the old behavior, see firebase.auth.Auth.onIdTokenChanged
That being said if you have the onAuthStateChanged observer, it'll trigger as soon as the user logs in and the code inside of your .then() block may or may not completely execute. If your auth observer redirects users to some other page if logged in, then there is a high chance that your user will be redirected before your then block completes.
If you need to execute some code after the user logs in, you should unsubscribe from the auth observer:
const auth = firebase.auth()
const authObserver = auth.onAuthStateChanged((user) => {
if (user) {
// redirect
}
})
btnLogin.onclick = async function () {
//unsubscribe
authObserver()
await auth.createUserWithEmailAndPassword(email, password).then((userCredential) => {
// onAuthStateChanged will trigger here if not disabled
//code stuff...
//return promise
}).then(() => {
// your code
})
}

Related

Firebase: Firestore query on onAuthStateChanged() works only on page reload

In a Vue app, in a separate JS file in my SRC folder, I have a function that does 3 things: first it listens to auth changes using onAuthStateChanged(), then it takes the user id from the signed-in user and queries the related Firestore user document, and finally it send the user document as an object to the Vuex store (as described in the 3 steps below).
const listenToAuthStateAndChanges = () => {
const auth = getAuth();
//STEP 1, listen to auth changes
onAuthStateChanged(auth, (user) => {
if (user) {
// User is signed in, see docs for a list of available properties
// https://firebase.google.com/docs/reference/js/firebase.User
const uid = user.uid;
//STEP 2, retrieve user doc from firestore based on the id above
const q = query(collection(db, "users"), where("userid", "==", uid));
async function getUserDoc() {
const querySnapshot = await getDocs(q);
querySnapshot.forEach((doc) => {
//STEP 3, store user doc info in Vuex store
store.state.userInfo = doc.data();
});
}
getUserDoc();
} else {
//user not signed in
}
});
}
This function above is then imported in the mounted hook of my Main-Header component:
export default {
mounted() {
//imported from auth.js
listenToAuthStateAndChanges()
},
My problem is that, when I sign a new user up (in a different signup component), the code from the function above stops running right after const q is declared. This means it detects the auth change, but it does not run the getUserDoc() function automatically. If I refresh the page, the getUserDoc() runs correctly and the Vuex store updates. There must be something obvious that I don't see here. Thank for any help!

Firebase Authentication JS does not populate `providerData`array

in a VueJS / QuasarJS application Im using firebase-js-sdk [1] together with firebaseui-web [2] to handle authentication.
After successful auth with any of the configured providers (e.g. password, google, apple, etc) I want to check which provider the user used. But immediately after successful authentication the user.providerData[] array that should contain the information is empty.
BUT if I reload my app the user.providerData[] array is suddenly populated correctly.
Iยดm checking for user data with something like this
import { getAuth } from "firebase/auth";
const auth = getAuth();
const user = auth.currentUser;
if (user) {
console.log(user.providerData)
}
After that the user object is fully populated (incl auth tokens, etc) but the user.providerData[] array is empty. Only after a page reload (CTRL-R) does the array get populated.
I searched both projects issues pages and documentation and didnt find anything that could explain this.
Im thankful for ANY idea where to look next!
EDIT
As suggested by #aside Im using onAuthStateChanged to check for updates of the user state.
onAuthStateChanged(
fbAuth,
(user) => {
if (user) {
console.log("onAuthStateChanged: user found");
console.log("onAuthStateChanged: user.providerData", user.providerData);
console.log("onAuthStateChanged: user", user);
} else {
console.log("onAuthStateChanged: no user found");
}
},
function (error) {
console.log("onAuthStateChanged:", error);
}
);
But even if I wait minutes after authentication is completed, still the user.providerData array is only populated after a page reload.
Here is a full demo: https://codesandbox.io/s/github/perelin/firebase-auth-providerdata-test
Thanks in advance :)
Im using
"firebase": "9.6.1",
"firebaseui": "6.0.0",
[1] https://github.com/firebase/firebase-js-sdk
[2] https://github.com/firebase/firebaseui-web
Your app should call getAuth().currentUser.reload() to refresh the local user data after login.
This could be done either in beforeRouteEnter() nav guard of the LoggedIn view:
// LoggedIn.vue
import { getAuth, signOut } from "firebase/auth";
export default {
async beforeRouteEnter(to, from, next) {
await getAuth().currentUser?.reload() ๐Ÿ‘ˆ
next()
},
}
demo 1
Or in the onAuthStateChanged callback:
// main.js
onAuthStateChanged(
fbAuth,
async (user) => {
await user?.reload() ๐Ÿ‘ˆ
},
)
demo 2
Your code is only running once instead of running every time the auth state is updated.
If you want to listen to any changes to the auth state, use a callback along with onAuthStateChanged as described here.
https://firebase.google.com/docs/auth/web/manage-users#get_the_currently_signed-in_user
import { getAuth, onAuthStateChanged } from "firebase/auth";
const auth = getAuth();
onAuthStateChanged(auth, (user) => {
if (user) {
// Check used provider here
const providerData = user.providerData;
// ...
} else {
// User is signed out
// ...
}
});
The reason checking/requesting the user object right after authentication does not work is that it might take firebase a second to update the providerData array. signInWithX might therefore return before the property is updated.

Firebase Auth: How to unsubscribe from Auth observer after user creation and then subscribe again?

I am using the createUserWithEmailAndPassword() method for signing up new users. Immediately after this user creation process, I am sending an email verification. Then, in my onAuthStateChanged() I have a condition to check whether the user has verified their email. The problem is that the Auth observer is logging out the user BEFORE the email sendEmailVerification() method is complete.
Based on the below code, where is the best place to succuessfully unsubscribe the observer ? And, how to do it with Firebase JS SDK v9?
Let me explain my use case and show my code:
pages/sign-up:
async signUp() {
const auth = getAuth()
const batch = writeBatch(db)
try {
const UserCredential = await createUserWithEmailAndPassword(
auth,
this.formValues.email,
this.formValues.password
)
const userDocRef = doc(db, 'users', UserCredential.user.uid)
batch.set(userDocRef, {
uid: UserCredential.user.uid,
displayName: this.formValues.displayName,
photoURL: `https://gravatar.com/avatar/${md5(
this.formValues.email
)}?d=identicon`
})
const usernameDocRef = doc(db, 'usernames', this.formValues.displayName)
batch.set(usernameDocRef, { uid: UserCredential.user.uid })
// Commit batch
await batch.commit()
console.log('batch committed, user is:', UserCredential.user.uid)
await this.verifyEmail() // <-- user is logged out before this has a chance to fire!
verifyEmail():
async verifyEmail() {
const auth = getAuth()
const actionCodeSettings = {
url: `${this.$config.baseUrl}/email-confirmation/success`
}
try {
await sendEmailVerification(auth.currentUser, actionCodeSettings)
} catch (error) {
console.error('An email verification error happened', error)
this.errorMessage = error.message
}
},
In my onAuthStateChanged() method, I am immediately logging out the user IF their email is not yet verified. This causes the following error:
And here is how I have my onAuthStateChanged observer set up (it runs before the page is rendered):
~/plugins/auth.js:
onAuthStateChanged(auth, (user) => {
if (user) {
if (!user.emailVerified) {
// User has not verified the email yet
store.dispatch('logOutUser')
}
// TO DO: finish up rest of user logic
Should the unsubscribe be in the auth.js or the pages/sign-up page? I am unsure how to unsubscribe.
If you need to perform certain actions after signup/login, then you should unsubscribe from auth observer as you've figured out.
const authObserver = onAuthStateChanged(auth, (user) => {
// ...
}
async signUp() {
//unsubscribe here i.e when user clicks signup button
authObserver()
const auth = getAuth()
const batch = writeBatch(db)
// ...
}
Do note that, if you you auth observer is meant to redirect logged in user somewhere else then it won't do it now. So make sure you do that manually.

Firebase.auth().currentUser turns to null every time the page restarts

I am using firebase authentication with vue application
Every time I restart the page after I log in a user currentUser turns to null
firebase.auth().signInWithEmailAndPassword(this.email, this.password)
.then(() => this.$router.push({name: 'Home'}))
.catch(err => this.feedback = err.message)
and in vue router
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
let user = firebase.auth().currentUser
if (!user) {
next({name: 'Login'})
} else next()
} else next()
})
I want the user to sign in once not every time the page restarts
This is caused because beforeEach is getting executed before firebase has fully finished initialization.
You can use onAuthStateChanged observer to make sure the vue app is initialized only after the firebase is fully initialized.
One way to fix it is to wrap the vue initialization code in main.js(new Vue( ... )) with onAuthStateChanged like this:
let app;
...
firebase.initializeApp( ... );
firebase.auth().onAuthStateChanged(() => {
if (!app) { // ignore reinitializing if already init (when signing out/login)
new Vue( ... )
...
}
})

How to know if Firebase Auth is currently retrieving user?

Background
I am using GoogleAuthProvider, with the default LOCAL persistence.
When I navigate to the page, I do:
firebase.initializeApp(firebaseConfig)
firebase.auth().currentUser // this is always null
firebase.auth().onAuthStateChanged(user => {
console.log("authStateChanged", user)
})
If the user is logged in, the callback is called once, with the user.
If the user is not logged in, the callback is also called once, with null.
This suggests I could wait until the first callback after navigating to the page to get the real login state before deciding what view to display, for instance. (I originally thought that it would not get called with null, and so I could end up waiting indefinitely)
Question
Would that be idiomatic usage? Does it seem like it will be robust against updates to firebase? Where can I find this discussed in the official documentation?
2022 Edit: in firebase web SDK 9, it's
import { getAuth, onAuthStateChanged } from "firebase/auth";
const auth = getAuth();
export const isReady = new Promise(resolve => {
const unsubscribe = onAuthStateChanged(auth, (/* user */) => {
resolve(/* user */)
unsubscribe()
})
})
P.S: The reason I don't resolve with the user is because it is available at auth.currentUser, while the promise would retain an outdated value.
Looking at similar questions such as Pattern for Firebase onAuthStateChanged and Navigation Guards - Quasar app it seems this is indeed the way it's done.
So I have come up with the following to differentiate the initial condition:
export const isReady = new Promise(resolve => {
const unsubscribe = firebase.auth().onAuthStateChanged(() => {
resolve()
unsubscribe()
})
})
I export this Promise from the module where I wrap firebase, so I can begin other initialization while waiting for an authoritative authentication state.
this worked for me instead. NB: For those user Quasar
export default async ({ app, router, store }) => {
return new Promise(resolve => {
const unsubscribe = auth.onAuthStateChanged((user) => {
auth.authUser = user
resolve()
unsubscribe()
})
})
}

Resources