Open heckad opened 3 years ago
I'm not sure I understand what the request is. Is the problem the same as the one you are asking about in the comment here: https://github.com/firebase/firebase-js-sdk/issues/462#issuecomment-735518608? If so, does the answer there solve your problem?
Or is this a separate issue?
Yes, this is the same problem. I tried your solution, and they didn't help because if a user wants to get access to the private route, then the router will wait until verification competes and the user will see a white screen all time. If I remove check verification from the route, then route redirects the user to a login page. Because I need the auth call callback less than 100ms even they can't get information from google.
@hsubox76 I think I have a similar use case, although it's a multi-page application instead.
Whenever the user navigates to a new page, everything including Firebase is initialized from scratch. I have some JS code that will dynamically render the page by setting document.body.innerHTML
, but it can only run after the auth state is known. The code is somewhat like this:
<header>
<script type="module">
onAuthStateChanged(getAuth(), user => {
document.body.innerHTML = `Hello, ${user.displayName}!`
})
<script>
</header>
<body></body>
Even on a fast connection, it takes 300ms for the callback to be called, so the browser won't wait and will show the blank page (because the body is empty in the HTML itself). So this is what the user sees in the screen:
On the other hand, if onAuthStateChanged
invoked the callback immediately with cached data (from IndexedDB or Local Storage), paint holding would take place and the user would see this in the screen:
Summarizing: if onAuthStateChanged
loaded immediately with cached data, there wouldn't be a white flash between navigations (when loading a new page). That would provide a much superior UX for multi-page applications. I know the initial auth state would potentially be stale/expired, but in that case the callback would be called again with fresh data. IIRC, that's just how Firestore's snapshot listeners work (edit: actually not, a new listener takes about 120ms to receive the first snapshot here; edit 2: about 40ms with persistence enabled which is still too slow for paint holding to work).
I found that this is where the problem happens:
Firebase indeed has the auth state stored locally (the user
parameter), but it forces a network request to check if it should be invalidated (_logoutIfInvalidated()
). That request is what takes a lot of time. The listeners will only be notified after it completes. TBH, I'm not even sure if there's much of a point in storing the auth state locally if it's always requested again from the server, and not used for optimistic UI.
Instead, I think it could immediately notify the listeners with the cached auth state and THEN proceed to check if the invalidation is actually needed (most times it won't be, at least for my app).
I understand that for some apps the invalidation may be important for security, but if that's the case, then at least add an option to initializeApp()
where we can opt in to using the cached auth state. It can also consider how much time ago it was last checked. If there was a session in the last 24 hours, use the cached auth state, otherwise not.
Please consider following my suggestion. It's a low-hanging fruit that will make all MPAs using Firebase Auth a lot faster.
I'll even leave a draft diff here so you have something to start with 😊:
export async function _reloadWithoutSaving(user: UserInternal): Promise<void> {
const auth = user.auth;
+
+ // Notify listeners with the cached user before checking if the invalidation is needed
+ // so the app can initialize fast.
+ user._notifyReloadListener(user);
+
+ // Wrap in a promise that's not awaited so it doesn't block the function termination.
+ Promise.resolve(async () => {
const idToken = await user.getIdToken();
const response = await _logoutIfInvalidated(
user,
getAccountInfo(auth, { idToken })
);
I know this diff would break other places using the _reloadWithoutSaving()
method, but I hope it can at least illustrate my suggestion. In reality, you'd probably add a options: { acceptCached?: boolean } | undefined
parameter and only pass { acceptCached: true }
when Firebase is being initialized.
Hi @gustavopch, thanks for digging into this more! My main concern is that this proposal blurs the lines between onAuthStateChanged()
and onIdTokenChanged()
(the former only updates when the user's uid
changes, the latter updates whenever the token itself updates).
With that said, there may be a way to deliver this functionality using a different/new API. I will discuss this more closely with the team.
@sam-gc That makes sense. I'll explain my actual use case. Pseudo-code:
let unsubscribe
onAuthStateChanged(getAuth(), authState => {
unsubscribe?.()
unsubscribe = onSnapshot(
doc(getFirestore(), `users/${authState.uid}`)
user => { console.log('User loaded') },
)
})
As you can see, I can only add the Firestore listener once I know the ID of the current user, so even when I have Firestore persistence enabled, it won't load the user document as fast as it could. I'm also not sure if there's any code inside Firestore that would wait for the auth state to be known even if my own code wasn't wrapped in an onAuthStateChanged()
, so that's something to keep in mind too.
@sam-gc Well, maybe simply populate getAuth().currentUser
with the cached auth state upon initialization? That way, you don't need to mess with onAuthStateChanged()
. So the code above would need to be adapted to:
let unsubscribe
const handleAuthState = authState => {
unsubscribe?.()
unsubscribe = onSnapshot(
doc(getFirestore(), `users/${authState.uid}`)
user => { console.log('User loaded') },
)
}
handleAuthState(getAuth().currentUser)
onAuthStateChanged(getAuth(), handleAuthState)
Note: added unsubscribe
to the snippet so people using it as a reference in the future don't forget.
The problem with either is that it changes the way the API operates, which is a breaking change. The team is exploring other options that avoid changing existing functionality (i.e. by adding this as a new feature).
One thing to note is that if the token is too old, the database read will fail on the server with a permission denied error. I'm not sure how this will interact with the local cache
One thing to note is that if the token is too old, the database read will fail on the server with a permission denied error.
@sam-gc Here's how I think it would work:
Assuming most times the token can be refreshed, then most times it will work seamlessly. In cases where the token can't be refreshed, then the user may see the cached data for a moment and then be forced to re-authenticate. Of course, that's assuming the developer wants to use the cached data, but it would be optional.
Hi, any movement on this? @sam-gc
TBH, I'm not even sure if there's much of a point in storing the auth state locally if it's always requested again from the server, and not used for optimistic UI.
Wholly agreed here.
Right now, the predominant workaround is to store some value in local storage (eg, a boolean, or maybe even the entire auth.currentUser
object) and manually check that to determine "logged-in-ness" faster than the sdk can re-resolve the current user. This is so clunky and forces us to re-implement logic and duplicate data that is already tracked by the sdk.
99.99% of the time, the user will be successfully validated, and only in 0.01% of the time might the user be deleted, or credentials revoked. We should optimise for the common case instead of the rare one.
The problem with either is that it changes the way the API operates, which is a breaking change.
I don't see how pre-populating the user with the stored data from IndexDB changes the way the API operates. The onAuthStateChanged
will still correctly notify us about changes to the auth state. Even auth.currentUser
doesn't break any definitions. In both cases, the APIs just work off of the most current information that they can, and correct themselves as soon as new information is available.
Edit: even today, onAuthStateChanged
and auth.currentUser
aren't guaranteed to serve fresh data. If a user has a browser tab open overnight and their credentials are revoked, when they view the browser in the morning they'll still be "logged in" and referencing stale data, until the sdk can re-validate everything asynchronously in the background.
I'm running into this issue as well.
TBH, I'm not even sure if there's much of a point in storing the auth state locally if it's always requested again from the server, and not used for optimistic UI.
Wholly agreed here.
In my experience, if a device is offline, then the request to refresh the user will fail fast and the app will launch quickly. So IMO it's great that the cache exists, and is certainly not just there as an optimization.
However, when on a slow network (such as a 3G connection), the refresh request has to time out before anything else can proceed, which is frustrating.
I'm currently hacking around this problem by intercepting fetch
(which I already do for logging purposes) and adding a 2-second timeout to the requests to identitytoolkit.googleapis.com
and securetoken.googleapis.com
.
It'd be great if the initialization sequence didn't block on this refresh, or if there were some other way to fetch the current user object from local storage while the initialization progresses. I don't really want to add my own user cache on top of the one that Firebase Auth provides, since that presents a bunch of additional synchronization issues to manage.
I just wanted to add that other than the slow 3G connection use case, anyone in a region where Firebase Auth isn't replicated to will also feel a lot of pain.
My customers are primarily in Australia, and the 300-600ms latency makes every single page refresh really clunky feeling, because I can't render anything if I don't know if the user is logged in or not.
@sam-gc Hello, friendly bump - any movement or new information on this issue?
This is a really annoying bug in Firebase. I've been trying to dig though the firebase sdk to see how to fix this, but it's pretty esoteric
Looking in the Chrome stack trace, I think onSnapshot waits for this auth call to complete before continuing to load: https://github.com/firebase/firebase-js-sdk/blob/086df7c7e0299cedd9f3cff9080f46ca25cab7cd/packages/firestore/src/remote/persistent_stream.ts#L446-L461
Any update on this? this was raised on 2020 and still no progress what so ever. All we are asking for is to give developers the option to trust the cached user, seriously, how hard can this be to implement? Cached user is already trusted in offline mode, so just give us this option.
This is killing the whole firebase ecosystem for MPA.
There does not appear to be a fix for this issue.
I'm working with a react native app which is also web enabled.
I can confirm react-native-firebase does not have the same issue, I'm assuming the currentuser details are stored locally to allow for offline access.
Like most apps, I initally load the Sign In screen and wait for onAuthStateChanged to be triggered. If a user is logged in and refreshes the page, then even running on a fast network with no network delays, I'm finding two issues:
I can provide the a simple app if that is required so you can replicate the issue. The proposals above make perfect sense.
I guess the only workaround is for apps where user convenience is important, the workaround for now is:
The problem with this approach is onAuthStateChanged has to be triggered before auth will respond to sign in\out requests. Sorry firebase auth team, this all needs either a serious rethink and proper solution or in case we're all getting this wrong, a real working example.
Finally, I did try using:
import ReactNativeAsyncStorage from '@react-native-async-storage/async-storage'; import { initializeAuth } from '@react-native-firebase/auth'; import getReactNativePersistence from 'firebase/auth';
auth = initializeAuth(app, { persistence: getReactNativePersistence(ReactNativeAsyncStorage) });
As documented in the react-native-firebase documentation, however; there are issues with modules not existing in packages and I wasn't able to get any combination of the above working.
Looking forward to a fix from the firebase team.
Thanks.
[REQUIRED] Describe your environment
Problem: If a user was logged and try to open spa app on page wich required auth access the app should waiting until auth will be confirmed and only then call callback
onAuthStateChanged
with the user object. This check can be quite long (more than 5 seconds with slow 3g).Propose: Call callback when we find user information in storage with this user object without waiting for the end of the auth check. If auth check return that user was logout than call callback with user null.
Thanks!