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.
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.
console. log(firebase. auth(). currentUser) // This returns null console.
By default, a session ends (times out) after 30 minutes of user inactivity. There is no limit to how long a session can last.
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
:
firebase.auth().currentUser
is null
because the client just loaded and it's still checking the credentials with the server.firebase.auth().currentUser
is null
because the client checked the credentials against the server and the client isn't logged in.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.
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...
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With