Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🔥 [🐛?] Auth: listener passed to onIdTokenChanged() not being called on token refresh #8181

Open
2 of 10 tasks
MartinCura opened this issue Dec 5, 2024 · 11 comments
Open
2 of 10 tasks
Labels
Needs Attention plugin: authentication Firebase Authentication type: bug New bug report

Comments

@MartinCura
Copy link

Issue

Hi all! The ID token is refreshed but my listener is never called, thus the app-wide Redux state is not updated with the new token and my app ends up using an expired Firebase token.

  const setUserToken = useCallback(
    (user: FirebaseAuthTypes.User | null) => {
      if (!user) {
        dispatch(authActions.clearToken());
        if (initializing) setInitializing(false);
        return;
      }
      user
        .getIdToken()
        .then((token) => dispatch(authActions.setToken(token)))
        .catch((error) => console.error(error));
    },
    [dispatch, initializing],
  );

  useEffect(() => {
    // No problem with login or logout
    const unsubscribe = auth().onAuthStateChanged((user) => {
      setAuthUser(user);
      setUserToken(user);
    });
    return unsubscribe;
  }, [setUserToken]);

  useEffect(() => {
    // *Why isn't this called on token refresh?* 👇
    const unsubscribe = auth().onIdTokenChanged(setUserToken);
    return unsubscribe;
  }, [setUserToken]);

  // On the other hand, this prints out the refreshed token 👇
  auth().currentUser?.getIdToken().then((token) => console.log(token));

I'm trying to understand if this is a setup issue somehow, but the rest seems to work correctly. I'm using Android but my colleagues are on iOS and having a similar problem. Issue goes away if user restarts the app, obviously. We are using Expo & EAS, if it's of any help.

Project Files

Javascript

Click To Expand

package.json:

{
    "@react-native-firebase/app": "^21.2.0",
    "@react-native-firebase/auth": "^21.2.0",
}

firebase.json for react-native-firebase v6:

# N/A

iOS

Click To Expand

ios/Podfile:

  • I'm not using Pods
  • I'm using Pods and my Podfile looks like:
# N/A

AppDelegate.m:

// N/A


Android

Click To Expand

Have you converted to AndroidX?

  • my application is an AndroidX application?
  • I am using android/gradle.settings jetifier=true for Android compatibility?
  • I am using the NPM package jetifier for react-native compatibility?

android/build.gradle:

// N/A

android/app/build.gradle:

// N/A

android/settings.gradle:

// N/A

MainApplication.java:

// N/A

AndroidManifest.xml:

<!-- N/A -->


Environment

Click To Expand

react-native info output:

% pnpm exec react-native info                                                                                                                                                   ✹ ✭
info Fetching system and libraries information...
System:
  OS: Linux 5.4 Ubuntu 20.04.6 LTS (Focal Fossa)
  CPU: (12) x64 Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
  Memory: 13.55 GB / 62.45 GB
  Shell:
    version: "5.8"
    path: /usr/bin/zsh
Binaries:
  Node:
    version: 20.16.0
    path: ~/.nvm/versions/node/v20.16.0/bin/node
  Yarn:
    version: 1.22.22
    path: ~/.yarn/bin/yarn
  npm:
    version: 10.8.1
    path: ~/.nvm/versions/node/v20.16.0/bin/npm
  Watchman: Not Found
SDKs:
  Android SDK: Not Found
IDEs:
  Android Studio: AI-241.18034.62.2411.12169540
Languages:
  Java:
    version: 17.0.13
    path: /usr/bin/javac
  Ruby:
    version: 2.6.2
    path: /home/martin/.rbenv/shims/ruby
npmPackages:
  "@react-native-community/cli": Not Found
  react: Not Found
  react-native: Not Found
npmGlobalPackages:
  "*react-native*": Not Found
Android:
  hermesEnabled: true
  newArchEnabled: false
iOS:
  hermesEnabled: Not found
  newArchEnabled: false

info React Native v0.76.3 is now available (your project is running on v0.74.5).
info Changelog: https://github.com/facebook/react-native/releases/tag/v0.76.3
info Diff: https://react-native-community.github.io/upgrade-helper/?from=0.74.5
info For more info, check out "https://reactnative.dev/docs/upgrading?os=linux".
  • Platform that you're experiencing the issue on:
    • iOS
    • Android
    • iOS but have not tested behavior on Android
    • Android but have not tested behavior on iOS
    • Both
  • react-native-firebase version you're using that has this issue:
    • 21.2.0
  • Firebase module(s) you're using that has the issue:
    • Auth
  • Are you using TypeScript?
    • Yes & 5.3.3

(Sorry if i'm missing some valuable info for debugging, haven't touched RN in some time so a lot of it is new for me; just ask and i'll provide!)


@mikehardy
Copy link
Collaborator

I encourage you to update to current versions and make sure it is still there. There was a related problem fixed on android but it was failure to call items after hot reload. It would not have affected iOS and you don't mention hot reload, but who knows - logging on old versions is frequently unproductive as things may have been fixed after

If you can still reproduce, I'd love to hear what we need to do for a minimal test case to show the problem

Starting from npx react-native init this creates a fresh react-native-firebase project, you can control the package names (as can I) so they match a set of android/ios google-services config files for firebase https://github.com/mikehardy/rnfbdemo/blob/main/make-demo.sh

But otherwise the app is just one page. You can hack in buttons as I did to test basic things and it would be great to know what we can put in there to trigger this quickly and confirm there's a repo problem vs a project-specific problem

Copy link

github-actions bot commented Jan 2, 2025

Hello 👋, to help manage issues we automatically close stale issues.

This issue has been automatically marked as stale because it has not had activity for quite some time.Has this issue been fixed, or does it still require attention?

This issue will be closed in 15 days if no further activity occurs.

Thank you for your contributions.

@github-actions github-actions bot added the Stale label Jan 2, 2025
@github-actions github-actions bot closed this as not planned Won't fix, can't repro, duplicate, stale Jan 17, 2025
@MartinCura
Copy link
Author

Fyi we never found a solution for this, and i'm currently trying with setting a timeout that simply force refreshes the token, and force refreshing when the app comes back in focus, etc. Would love for this to just work out of the box.

@mikehardy
Copy link
Collaborator

I'm still not sure exactly how to reproduce this, and it is because I've personally never seen it. When I tried to think through why in response to your comment that it is still happening, I realized I never stored the token. Why would I? The user is local, the token is local. You can always get the user from auth, and get the token from the user. There's no need for the app to store it anywhere, firebase is already storing it for you and you always have device-local access to it from firebase APIs. Perhaps that's a real way forward.

That said, the listener should be called if it's documented behavior, not disagreeing there

@MartinCura
Copy link
Author

MartinCura commented Feb 6, 2025

Yeah, i'm also stumped.

To be clear we're only storing the token for a particular case where i couldn't find a way to await auth().currentUser.getIdToken() so that i can pass the token to a react hook. For the common use case of calling our own API directly we can imperatively use await auth().currentUser.getIdToken().

But even then if the onIdTokenChanged() listener were reliably called then this wouldn't be a problem for me.

Is there a way i could debug this and/or give you more info?

@MartinCura
Copy link
Author

MartinCura commented Feb 6, 2025

Some other info:

  • I am personally using Android (emulator and device)
  • Colleagues use iOS
  • All of us have had the app fail API fetches because the token had expired

If i'm missing anything i can definitely work through it, but the docs are sober on details and i understand that simply getting to call getIdToken() should be enough (for our common case), but it isn't working. 🤔

And there's no way to shorten the expiration period of the token, is there? Which would make debugging easier.

@mikehardy mikehardy reopened this Feb 6, 2025
@mikehardy
Copy link
Collaborator

Thanks for the extra info

For reproduction: I don't believe there is a way to shorten it declaratively, no, it defaults to an hour but apparently after 55 minutes, the firebase SDKs begin attempting to refresh, citation from firebase support team: https://stackoverflow.com/questions/62389267/how-to-handle-firebaseauth-token-expiration#62390404

I had forgotten that getIdToken was async, as it may need to wait on a refresh, though in practice the token should be valid at all times and thus may be a synchronous experience there is the chance it really has to sit on the network and wait (or timeout and return null...)

An idea though - If you send 'forceRefresh' true to getIdToken() on User, that should fetch a new token and...that should call the listener shouldn't it?

https://firebase.google.com/docs/reference/js/auth.user.md#usergetidtoken

That bears testing in pure firebase-js-sdk to see if it behaves that way and also you could test it immediately to see if you get different tokens from calling getIdToken(false) then getIdToken(true). If that works and if firebase-js-sdk calls the listener in that case, it can be a good test for the listener here - we could add that to the e2e suite even

@MartinCura
Copy link
Author

So you recommend we just force refresh the token on each API call? (getIdToken(true) always)

@mikehardy
Copy link
Collaborator

mikehardy commented Feb 6, 2025

Oh no - that would be very very expensive and I think on the backend google would auto-detect that as a mild form of abuse and start rate-limiting you which would be a nightmare.

I was thinking purely as how to reproduce this in a deterministic amount of time so we can expose the problem and fix this issue (assuming it reproduces).

So it's more of a "does this even work as a way to reproduce?" question that needs a little proof, not a recommendation for your general case.

Sorry that wasn't clear

@MartinCura
Copy link
Author

Ah sorry for misunderstanding, yeah that makes sense.

I just tried it and yes, if i force refresh tokens on each API call then the listener for onIdTokenChanged is called each time (if i don't, then it isn't).

@mikehardy
Copy link
Collaborator

mikehardy commented Feb 6, 2025

Okay - that's good to know. Very very interesting that isn't called on the auto-refresh case but is on the force-refresh case.

Thanks for the information.

Path forward for investigation / fix:
1- Comparison test - minimal repro on pure firebase-js-sdk where user is authed to get a token, then wait for an hour to see how pure firebase-js-sdk id token changed listener behaves (hypothesis: the listener is called correctly in pure firebase-js-sdk).
2- assuming firebase-js-sdk calls listener correctly on auto-token-refresh while we still do not, dive in to why not on react-native-firebase side

As a maybe-better workaround, you can use the standard pure-javascript jwt library to parse the token and know the real expiry time if you aren't already. That could be used to inform your timeout / force fetch workaround to be as efficient as possible - only comparing stored vs current / force fetching when close to expiry

@mikehardy mikehardy added plugin: authentication Firebase Authentication and removed Stale labels Feb 6, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Attention plugin: authentication Firebase Authentication type: bug New bug report
Projects
None yet
Development

No branches or pull requests

2 participants