firebase-js-sdk icon indicating copy to clipboard operation
firebase-js-sdk copied to clipboard

Possible race condition when calling signInAnonymously immediately after getAuth()

Open shaibt opened this issue 1 year ago • 17 comments

[REQUIRED] Describe your environment

  • Operating System version: iOS 16.3.1
  • Browser version: Safari 16.3.1
  • Firebase SDK version: 9.15.0
  • Firebase Product: auth

[REQUIRED] Describe the problem

Steps to reproduce:

On loading, our React app immediately loads a component that does the following:

  • calls getAuth()
  • runs a hook to setup onAuthStateChanged
  • once onAuthStateChanged handler is called for 1st time we check for a valid user object
  • If no valid user object exists call signInAnonymously

What we're observing in our production env on an irregular basis is that the onAuthStateChanged is called multiple times with different a uid everytime.
More specifically, the 1st time app is opened on a "clean" browser, there is no persisted user uid for Firebase auth so the onAuthStateChanged handler fires with null user, we perform signInAnonymously, and onAuthStateChanged is fired again with valid uid. Next time app is opened (few mins later) we can see onAuthStateChanged fired mutliple times in sequence with an alternating uid provided - one time the previous uid from the first-time open and a new uid.

Expectation is that the previous session's uid will be provided OR that a new uid be created but would not change - onAuthStateChanged should not be called with multiple diff values of uid for the same browser session.

Relevant Code:

 export default function ClientAuthProvider() {
const [user, setUser] = useState(null);
const auth = getAuth(firebaseApp);

useEffect(() => {
        const unsub = onAuthStateChanged(auth, user => {
            if (user) {
                setUser(user);
            } else {
                signInAnonymously(auth)
                   .catch(error => {               
                       setUser(null)
                   })
             }
        })
        return () => {
            unsub();
        }
    }, []);

 }

This wasn't reproducible in testing envs and happens only on occasion on production. Could it be that calling getAuth() multiple times would create more than 1 instance of the auth service? Could it be that calling signInAnonymously too early would cause a race condition in auth service to generate two uid for same browser? This issue looks similar in some ways to the issue described in https://github.com/firebase/firebase-js-sdk/issues/6827

shaibt avatar Feb 21 '23 13:02 shaibt

Thanks for reporting this. I tried modifying the demo app in and could not repro this issue. I added signInAnonymously in https://github.com/firebase/firebase-js-sdk/blob/547348ba59e1a2bf108330bd882c2ea2753d54d0/packages/auth/demo/src/index.js#L1749 and it returns the same user once signed in.

We do await on the initializationPromise here - https://github.com/firebase/firebase-js-sdk/blob/cdada6c68f9740d13dd6674bcb658e28e68253b6/packages/auth/src/core/strategies/anonymous.ts#L38, so the same issue in #6827 shouldn't happen. Also, the fix for that issue was included in 9.16.0 - https://github.com/firebase/firebase-js-sdk/pull/6953, are you able to try with that? (though i am not sure it is related).

2 more followups:

  1. in onAuthStateChanged callback are you able to log the uid and the sign_in_provider or is_anonymous fields (to make sure the different uids are all part of anonymous sign in only)?

  2. Are you able to reproduce this in other browsers, or only Safari/iOS?

prameshj avatar Mar 07 '23 05:03 prameshj

Hi @prameshj,

I think the scenario for this issue is a little different than originally described. First, to your questions and then an update:

  1. in onAuthStateChanged callback we started to log the uid we receive in the last couple of weeks and I have some more insight on this below. We'll add the isAnonymous and providerId data as well to our logs going forward. We will also upgrade firebase 9.15.0 -> 9.17.2
  2. We are not able to reproduce at all in our dev/test env but looking at production logs we can see this is a Safari/Webkit-only issue - for both iOS and OSX.

Update: Looking at logs from last couple of weeks since we opened this issue including logging for onAuthStateChanged callback it looks like the affected sessions are suffering from the following issue:

  • session starts without any persisted auth info. onAuthStateChanged fired with uid==undefined
  • signInAnonymously called. onAuthStateChanged fired with uid==123
  • (optionally - not in all cases in our flow) a custom sign-in token is provided by our backend and signInWithCustomToken called. onAuthStateChanged fired with uid==123 (same as before as expected). The session in this case is no longer anonymous.
  • Then at random times during the session - could be few minutes and could be hours, and even after signInWithCustomToken is called - onAuthStateChanged is fired without any auth operation by the user with uid==undefined so we call signInAnonymously again and callback is fired with uid == 456 (diff than before). Seems that at some point Firebase Auth decides to reset the auth data but the client is already signed in!. How can this happen without an explicit signout?

Could it be that in case of Safari/Webkit Auth loses some persisted data that causes this "refresh" ? An important point I forgot to mention in the OP is that our Firebase app opens as an iFrame over a "hosting" website so it'll be considered a 3rd party domain by the browser. Could it be that Auth has difficulty persisting data in this case of Safari/webkit on cross-domain and spontaneously refreshes the uid?

  • For one client in particular (using a iOS/GSA browser - see below), there's an endless loop of callbacks fired alternating uid as described in the original issue post. This might be a separate issue or part of the same one.

As to browsers/devices:

  • issue appears to be only in iOS/Safari/Webkit (40% of our total sessions) and somewhat in OSX/Safari (24%). Did not see occurrence at all on Windows (16%) or Android (20%)
  • iOS/Mobile Safari and OSX/Safari are affected at ~10% of their respective sessions.
  • Looks like the browser most affected is iOS/GSA (this is the parsed user agent string) - seems like a webkit based Google browser for iOS but I'm not 100% sure what the app is exactly (gmail in-browser app?). 85% of sessions using this browser are affected by the problem.

shaibt avatar Mar 07 '23 17:03 shaibt

Seems that at some point Firebase Auth decides to reset the auth data but the client is already signed in!.

This is very weird - Does this happen while they are on the web app still, i.e no page refresh?

It is possible that there is some cross-origin storage access at play here. So, the hosting website is domain1. Your firebase app is domain2 and in domain1, you open an iframe to domain2 (where the onAuthStateChanged callbacks are invoked), correct?

Are you using Local Storage persistence by any chance? I wonder if this is what happens:

  1. https://github.com/firebase/firebase-js-sdk/blob/0b3ca78eb97ce328b7db4ad5bb11f254e5de6a9a/packages/auth/src/core/persistence/persistence_user_manager.ts#L60 - we setup a listener for changes to storage
  2. The local storage entry for the user somehow gets wiped out and we set current user to null - https://github.com/firebase/firebase-js-sdk/blob/0b3ca78eb97ce328b7db4ad5bb11f254e5de6a9a/packages/auth/src/core/auth/auth_impl.ts#L180, which invokes onAuthStateChanged callback.

There is some special case in Local Storage persistence for Safari/iOS + iframe - https://github.com/firebase/firebase-js-sdk/blob/7d23aa4bd1e29d2c10c771c0ab7919b6c5dd2d9b/packages/auth/src/platform_browser/persistence/local_storage.ts#L90, but I think this is related to syncing the local storage manually (rather than items getting wiped out). Curious if you are using a specific persistence setting though. cc @sam-gc

prameshj avatar Mar 16 '23 20:03 prameshj

Hey @shaibt. We need more information to resolve this issue but there hasn't been an update in 5 weekdays. I'm marking the issue as stale and if there are no new updates in the next 5 days I will close it automatically.

If you have more information that will help us get to the bottom of this, just add a comment!

google-oss-bot avatar Mar 23 '23 01:03 google-oss-bot

Hey @prameshj:

It is possible that there is some cross-origin storage access at play here. So, the hosting website is domain1. Your firebase app is domain2 and in domain1, you open an iframe to domain2 (where the onAuthStateChanged callbacks are invoked), correct?

You are correct - the Firebase app is only contained inside the iframe for domain2.

Are you using Local Storage persistence by any chance? I wonder if this is what happens:

We don't set any special config settings during Firebase (or Firebase Auth) init - just plain default settings. What is the default persistence method? Does Firebase auth persist data in local storage?

In our app, we do use local storage for persisting several app variables and indeed in Safari they don't get retained for very long - not longer than a few hours. Seems that Safari in this cross domain configuration will clean the cross domain iframe's local storage from time-to-time and we can see this as the variables are not persisted over long periods of time as in Chrome.

However, the only Firebase related entry in local storage that I can see is firebase:host:[project-name].firebaseio.com and nothing to do with the current auth session. Could it be that the code you referenced is invoked when Safari decides to wipe storage and that causes Auth to unnecessarily reset the session?

shaibt avatar Mar 24 '23 06:03 shaibt

We don't set any special config settings during Firebase (or Firebase Auth) init - just plain default settings. What is the default persistence method? Does Firebase auth persist data in local storage?

With getAuth, we pass in indexDb, localStorage, sessionStorage. https://github.com/firebase/firebase-js-sdk/blob/fdd4ab464b59a107bdcc195df3f01e32efd89ed4/packages/auth/src/platform_browser/index.ts#L82

If indexDb is not available, it falls back to local storage. https://github.com/firebase/firebase-js-sdk/blob/fdd4ab464b59a107bdcc195df3f01e32efd89ed4/packages/auth/src/core/persistence/persistence_user_manager.ts#L116-L133

Is it possible that indexDB is not available in the Safari version you are using? Do you see any indexDB/local storage entry like:

firebase:authUser:<api key>:[DEFAULT]

Could it be that the code you referenced is invoked when Safari decides to wipe storage and that causes Auth to unnecessarily reset the session?

The code I reference will get invoked upon local storage events, so if Safari does wipeout the storage entry, that will explain the issue you see.

prameshj avatar Mar 27 '23 20:03 prameshj

In Safari dev tools I cannot see the firebase:authUser:<api key>:[DEFAULT] in neither local storage or indexedDB.

I wasn't sure if its browser policy or the dev tools only showing indexedDB for the main/hosting domain1 and not showing for the Firbebase app's domain2. So I added some code to our firebase app to open the IndexedDB named firebaseLocalStorageDb inside the iframe/domain2 and indeed the firebase:authUser... key was there. So it is persisted in IndexedDB.

So if auth is persisted in indexedDB why is a local storage event refreshing auth? Or more likely, Safari is also clearing the IndxedDB which would cause the onAuthStateChanged to fire with user null as well.

PS: In Chrome, under same exact scenario I can clearly see the auth entry in indexedDB under domain2.

shaibt avatar Mar 28 '23 11:03 shaibt

Hey @prameshj - any updates/ideas of how we can work around this Safari issue?

shaibt avatar Apr 03 '23 06:04 shaibt

I wasn't sure if its browser policy or the dev tools only showing indexedDB for the main/hosting domain1 and not showing for the Firbebase app's domain2.

This is likely the case, Safari 16.1+ does session storage partitioning, it probably extends to local and indexedDB storage too..

So if auth is persisted in indexedDB why is a local storage event refreshing auth? Or more likely, Safari is also clearing the IndexedDB which would cause the onAuthStateChanged to fire with user null as well.

yea, if you are using indexedDB, then the local storage code path I pointed out is likely not suspect. Are you seeing indexedDb entries in the same Safari version that sometimes sees the race condition issue? I wonder if the affected versions somehow don't have indexDb available. Or, as you mentioned, maybe indexedDb entries are getting wiped out too. Are you able to add a listener for indexedDb entries and see if those fire?

prameshj avatar Apr 03 '23 06:04 prameshj

Cannot inspect indexedDB in dev tools because for some reason Safari will only show the main domain's indexedDB. But when using code to read the contents of the indexedDB in the iframe I can see the Firebase auth entries there. I'm not sure its possible to set listeners to IndexedDB - or maybe I didn't understand your last comment.

shaibt avatar Apr 03 '23 06:04 shaibt

You're right.. looks like indexedDb doesn't have storage event observer support - https://stackoverflow.com/questions/33237863/get-notified-when-indexeddb-entry-gets-changed-in-other-tab

It is possible to poll.. which is what we do in the auth persistence implementation - https://github.com/firebase/firebase-js-sdk/blob/7d23aa4bd1e29d2c10c771c0ab7919b6c5dd2d9b/packages/auth/src/platform_browser/persistence/indexed_db.ts#L354

Which Safari version are you seeing the issue with?

Also, as an aside, how do you handle the cases where a user logouts from the app? That will still call onAuthStateChanged and create a new anonymous user. That wouldn't cause this race condition (unless there is some sign out code path triggering erroneously), but it would lead to a lot of zombie anonymous users (if the user closes the page shortly after signing out).

prameshj avatar Apr 03 '23 22:04 prameshj

Seeing it across all Safari 16.X versions in iOS & OSX.

There is no logout option for the app. To keep things simple:

  • our app is based on user sessions - there is a definitive start and end to the session. sessions might last few hours.
  • when a session starts we call signInAnonymously and then ask our backend to create a custom sign in token for that uid (the custom token includes some custom claims used in our backend and DB rules) and call signInWithCustomToken.
  • during a session, we don't expect a change in uid -> this is what's causing the issue.
  • after session is over, the auth is considered expired and when a new session starts we go through the sign in logic again - here its reasonable to expect that the uid might change.

shaibt avatar Apr 04 '23 06:04 shaibt

Thanks for the additional info! We will try to repro this at our end with additional logs.

prameshj avatar Apr 10 '23 22:04 prameshj

Hi @prameshj, just wondering if you have any update on this case.

shaibt avatar May 10 '23 06:05 shaibt

We haven't been able to get a repro yet. cc @jbalidiong

prameshj avatar May 15 '23 17:05 prameshj

I had the same problem with Safari 17.1.2. The code is slightly different because it uses Angularfire, but I believe the issues are the same. This issues will not occur unless perhaps the old credentials are still in indexedDb.

problem code

export class FirebaseService {
  constructor(
    private afa: AngularFireAuth,
  ) {}
  public initializeFirebase(): void {
    this.afa.authState.subscribe(user => {
      // **not get here**
    });
    this.afaLogin();
  }
  public afaLogin(): void {
    this.afa.signInAnonymously().catch(()=> {
      setTimeout(() => {
        this.afaLogin();
      }, 1000);
    });
  }
}

modified code

export class FirebaseService {
  constructor(
    private afa: AngularFireAuth,
  ) {}
  public initializeFirebase(): void {
    this.afa.signInAnonymously().then(() => {
      this.afa.authState.subscribe(user => {
        // **get here**
      });
    }).catch(()=> {
      setTimeout(() => {
        this.initializeFirebase();
      }, 1000);
    });
  }
}

acn-masatadakurihara avatar Dec 11 '23 09:12 acn-masatadakurihara

Hi @prameshj, Bumping this issue as we're still plagued with problems in production - Safari browsers only (ver 16.X, 17.X) Just a quick reminder: Launch Firebase auth inside a cross domain iframe - hosting site origin XXX.com and iframe w/ Firebase origin is YYY.com. Initial auth is fine and onAuthStateChanged is fired with a valid user. But at some random timing in the future, with no user interaction, onAuthStateChangedis fired again with null user. Seems Safari is wiping out the storage.DB used for persisting credentials and casuing auth state to reset while app is in flight. Any plans to address this? Looks like its remotely related to this issue?

shaibt avatar Apr 10 '24 08:04 shaibt