Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Firebase auth.currentUser is null when loading the page, user loaded when authstatechange called after some milli seconds of page load

I am using React with Firebase to develop a small web app. To take care of authentication, I am using context API and inside context I am adding loggedin user details.

AuthProvider.tsx

const AuthProvider: React.FC = props => {
  const [state, setState] = useState<IAuthContext>(authInitialState);
  console.log("Inside AuthProvider");
  useEffect(() => {
    auth.onAuthStateChanged( user => {
      console.log("Auth State changed and user is ----->", user);
      if (user) {
        console.log("User value updated to the context")
        setState({
            ...authInitialState,
          isAuthenticated:!!user,
          permissions:[],
          user:user
        });
      }
      const stateChange = {
        ...authInitialState,
        isAuthenticated: !!user,
        user
      };
      // if (!user) {
        return setState(stateChange);
      // }
    });
  }, []);
  console.log("Rendering AuthProvider", state);
  return (
    <AuthContext.Provider value={state}>{props.children}</AuthContext.Provider>
  );
};
export default AuthProvider;

AuthConsumer.tsx

const withAuthContext = (
  Component: React.ComponentClass<any> | React.FunctionComponent<any>
) => {
  console.log("Rendering AuthConsumer for ");
  return (props: any) => (
    <AuthContext.Consumer>
      {context => <Component {...props} context={context} />}
    </AuthContext.Consumer>
  );
};

export default withAuthContext;

PrivateRoute.tsx

interface PrivateRouteProps extends RouteProps {
    // tslint:disable-next-line:no-any
    component: any;
    context: IAuthContext;
}

const PrivateRoute = (props: PrivateRouteProps) => {
    const { component: Component, context: IAuthContext, ...rest } = props;
    console.log("Private route for ", props);
    return (
        <Route
            {...rest}
            render={(routeProps) =>
                props.context.isAuthenticated ? (
                    <Component {...routeProps} />
                ) : (
                    <Redirect
                        to={{
                            pathname: '/login',
                            state: { from: routeProps.location }
                        }}
                    />
                )
            }
        />
    );
};

export default withAuthContext(PrivateRoute);

App.tsx

return (
        <BrowserRouter>
            <div>
                <Switch>
                    <PublicRoute path="/frame" component={Frame} exact isAuthorized={true}/>
                    <Route path="/login" component={NewLogin} exact isAuthorized={true}/>
                    <PrivateRoute path="/nav" component={NavigationBar} exact/>
                    <PrivateRoute path="/dashboard" component={AnalyticsDashBoard} exact/>
                    <PrivateRoute path="/subscription" component={OrderSuccess} exact/>
                    <PrivateRoute path="/onboarding" component={OnBoarding} exact/>
                </Switch>
            </div>
        </BrowserRouter>
    );

The user is already loggedin, and session persistence set to local. The problem is, when I try localhost/subscription, which is a private route, context.isAuthenticated is false since "onAuthStateChanged" observer is not triggered yet, so its going to login page, but in few milliseconds, authStateChange triggered and context set, but its no use since application already navigated to login since privateroute thought user not loggedin. I would like to get knowledge on how to overcome this issue.

like image 710
Codex Avatar asked Feb 10 '20 11:02

Codex


People also ask

Why auth CurrentUser is null?

You can also get the currently signed-in user by calling CurrentUser . If a user isn't signed in, CurrentUser returns null. Note: CurrentUser might also return null because the auth object has not finished initializing.

What does firebase auth () CurrentUser return?

console. log(firebase. auth(). currentUser) // This returns null console.

How long does firebase auth session last?

By default, a session ends (times out) after 30 minutes of user inactivity. There is no limit to how long a session can last.


2 Answers

When the page loads Firebase restores the user's credentials from local storage and checks with the server whether they are still valid. Since this is a call to the server, it may take some time and happens asynchronously. This is the reason it is normal that firebase.auth().currentUser is null until onAuthStateChanged fires.

The problem you have is that there are multiple reasons firebase.auth().currentUser can be null:

  1. firebase.auth().currentUser is null because the client just loaded and it's still checking the credentials with the server.
  2. firebase.auth().currentUser is null because the client checked the credentials against the server and the client isn't logged in.
  3. firebase.auth().currentUser is null because the user never signed in, so there are no credentials to check.

You want to navigate in case 2 and 3, but not case 1.

The typical solution is to not handle navigation until the first time onAuthStateChanged has fired. At that point you can be certain that the credentials were checked against the server, or that there were no credentials to check, in both of which cases you'll want to navigate to the sign-in page.


An additional option to speed this up is to store a small token in local storage yourself when the user signs in for the first time, and then read that when the app loads. If the token is present you can distinguish between case 1 and 3, and use that to navigate to the correct page a bit sooner.

For an example of this, see this I/O talk about Architecting Mobile Web Apps.

like image 82
Frank van Puffelen Avatar answered Oct 01 '22 18:10

Frank van Puffelen


If you want to wait for onAuthStateChanged() to fire you can use a promise with async / await:

/** Waits for auth state to init on navigation or refresh, otherwise resolves immediately */
private async waitForAuthInit() {
  let unsubscribe: Promise<firebase.default.Unsubscribe>;
  await new Promise<void>((resolve) => {
    unsubscribe = this.auth.onAuthStateChanged((_) => resolve());
  });
  (await unsubscribe!)();
}

I await this function before I query for the user:

async getCurrentUser(): Promise<firebase.default.User | null> {
    try {
      await this.waitForAuthInit();
      return await this.auth.currentUser;
    } catch (err: any) {
      console.log('Failed to get current user...', err);
      return null;
    }
  }

Now it's safe to query for the user on page load, without having to use callback functions, observables, subscriptions, etc. and without having to call onAuthStateChanged() ever again.

I should note I'm using Angular 13 and the @angular/fire dependency, which is a thin wrapper library. The syntax is probably different in other frameworks.

This is the only way I've found to contain the firestore dependency to a single service, instead of calling onAuthStateChanged() all over the place.

It's particularly useful for Angular AuthGuards, you can call getCurrentUser() without having to worry about a false negative during those initial milliseconds.

It would be nice if firebase implemented something like this on their side, so we wouldn't get false negatives when we call auth.currentUser, but that's none of my business...

like image 35
Chris Hamilton Avatar answered Oct 01 '22 16:10

Chris Hamilton