Reputation: 2312
Hard refreshes on my SPA React/Firebase application does not maintain auth state on immediate execution of a function. I have a workaround, but it's sketchy.
My react routes utilize the onEnter
function to determine whether or not the user is authenticated or not. For instance
<Route path="/secure" component={Dashboard} onEnter={requireAuth}/>
Furthermore, my requireAuth
function looks like this:
function (nextState, replace) {
console.log('requireAuth', firebase.auth().currentUser);
if (!firebase.auth().currentUser) {
console.log('attempting to access a secure route. please login first.');
replace({
pathname: '/login',
state: { nextPathname: nextState.location.pathname }
});
}
};
However, on a hard refresh there is a slight delay on firebase.auth().currentUser
. It's null at first, then executes a POST
to firebase servers in order to determine auth state. When it returns the currentUser
object is populated. This delay causes issues though.
My hacky solution is the following: update: this doesn't actually work...
function (nextState, replace) {
setTimeout(function () {
console.log('requireAuth', firebase.auth().currentUser);
if (!firebase.auth().currentUser) {
console.log('attempting to access a secure route. please login first.');
replace({
pathname: '/login',
state: { nextPathname: nextState.location.pathname }
});
}
}, 50);
};
Simply wrap it in a timeout. However, I really don't like this... any thoughts?
Update:
I have also tried to wrap it within a onAuthStateChanged
listener, which should be more accurate than a setTimeout
with a definitive time delay. Code as follows:
function (nextState, replace) {
var unsubscribe = firebase.auth().onAuthStateChanged(function (user) {
if (!user) {
console.log('attempting to access a secure route');
replace({
pathname: '/login',
state: { nextPathname: nextState.location.pathname }
})
console.log('should have called replace');
}
unsubscribe();
});
// setTimeout(function () {
// console.log('requireAuth', firebase.auth().currentUser);
// if (!firebase.auth().currentUser) {
// console.log('attempting to access a secure route. please login first.');
// replace({
// pathname: '/login',
// state: { nextPathname: nextState.location.pathname }
// });
// }
// }, 50);
};
The two log statements are executed, but react-router replace
does not seem to be executed correctly. Perhaps that's a different question for the react-router experts.
update 2:
It was late at night when I was working on this. Apparently setTimeout
doesn't actually work either.
Upvotes: 14
Views: 7237
Reputation: 11
Modern solution:
auth has private fields isInitialized and initializationPromise so we can check them and await. After this await user would be loaded
Solutions with localStorage for me looks very unpleasant, this hack is to much better
TS:
if ((this.auth as any)._isInitialized === false)
await (this.auth as any)._initializationPromise;
JS:
if (this.auth._isInitialized === false)
await this.auth._initializationPromise;
Upvotes: 1
Reputation: 409
This worked for me try to wrap your main app in an if else statement depending on the state of firebase.auth .
constructor() {
super();
this.state={
user:{},
stateChanged:false
};
}
componentDidMount(){
fire.auth().onAuthStateChanged((user)=>{
this.setState({stateChanged:true})
});
}
render() {
if(this.state.stateChanged==false){
return(
<div>
Loading
</div>
)
}
else{
return(<div>your code goes here...</div> )
}
}
Upvotes: 0
Reputation: 93
Works by managing localStorage. Here is example how I do it.
constructor(props) {
super(props);
let authUser = null;
// setting auth from localstorage
for (let key in localStorage) {
if (key === storageId) {
authUser = {};
break;
}
}
this.state = {authUser};
}
componentDidMount() {
firebase
.auth
.onAuthStateChanged(authUser => {
if (authUser) {
localStorage.setItem(storageId, authUser.uid);
} else {
localStorage.removeItem(storageId);
}
// change state depending on listener
authUser
? this.setState({authUser})
: this.setState({authUser: null});
});
}
Upvotes: 2
Reputation: 41
While this is a post related to ReactJS, I recently came across the same problem when writing my own authentication/authorisation service for AngularJS. On page refresh the onAuthStateChanged
passes a user that is null
because firebase is still initializing (asynchronously).
The only solution that worked for me was storing the users uid in localStorage
after the user has logged in and deleting the value after the user has logged out.
Since i'm using a authService
and userService
seperately I registered a listener in the authService
that is fired once the user is logged in/out.
Code sample authService (not the full authService):
var loginListeners = [];
var logoutListeners = [];
function addLoginListener(func) {
loginListeners.push(func);
}
function addLogoutListener(func) {
logoutListeners.push(func);
}
function login(email, password) {
return firebase.auth().signInWithEmailAndPassword(email, password).then(function(user) {
for(var i = 0; i < loginListeners.length; i++) {
loginListeners[i](user); // call registered listeners for login
}
});
}
function logout() {
return firebase.auth().signOut().then(function() {
for(var i = 0; i < logoutListeners.length; i++) {
logoutListeners[i](); // call registered listeners for logout
}
});
}
Code sample userService (not the full userService):
.provider('userService', ['authServiceProvider',
function UserService(authServiceProvider) {
var usersRefUrl = '/users';
var userInfo = null;
var userDetails = null;
// refreshHack auto-executed when this provider creates the service
var storageId = 'firebase:uid'; // storing uid local because onAuthStateChanged gives null (when async initializing firebase)
(function addRefreshHackListeners() {
authServiceProvider.addLoginListener(function(user) {
userInfo = user;
localStorage.setItem(storageId, user.uid); // store the users uid after login so on refresh we have uid to retreive userDetails
});
authServiceProvider.addLogoutListener(function() {
userInfo = null;
localStorage.removeItem(storageId);
});
firebase.auth().onAuthStateChanged(function(user) {
if(user) { // when not using refreshHack user is null until async initializing is done (and no uid is available).
localStorage.setItem(storageId, user.uid);
userInfo = user;
resolveUserDetails();
} else {
localStorage.removeItem(storageId);
userInfo = null;
userDetails = null;
}
});
})();
function isLoggedIn() {
return userInfo ? userInfo.uid : localStorage.getItem(storageId); // check localStorage for refreshHack
}
function resolveUserDetails() {
var p = null;
var uid = isLoggedIn();
if(uid)
p = firebase.database().ref(usersRefUrl + '/' + uid).once('value').then(function(snapshot) {
userDetails = snapshot.val();
return userDetails;
}).catch(function(error) {
userDetails = null;
});
return p; // resolve by returning a promise or null
}
}]);
And in a run-block you can globally register a user and resolve the user-info/details every route change (makes it more secure):
.run(['$rootScope', 'userService', 'authService',
function($rootScope, userService, authService) {
// make user available to $root in every view
$rootScope.user = userService.getUser();
$rootScope.$on('$routeChangeStart',
function(event, next, current) {
// make sure we can add resolvers for the next route
if(next.$$route) {
if(next.$$route.resolve == null)
next.$$route.resolve = {};
// resolve the current userDetails for every view
var user = userService.resolveUserDetails();
next.$$route.resolve.userDetails = function() {
return user;
}
}
});
}]);
Maybe this can help someone who is struggling the same issue. Besides that feel free to optimize and discuss the code samples.
Upvotes: 4
Reputation: 2312
Okay. So, I was able to solve this by utilizing the localStorage
variable that firebase provides to store the user information.
function (nextState, replace) {
if (!firebase.auth().currentUser) {
let hasLocalStorageUser = false;
for (let key in localStorage) {
if (key.startsWith("firebase:authUser:")) {
hasLocalStorageUser = true;
}
}
if (!hasLocalStorageUser) {
console.log('Attempting to access a secure route. Please authenticate first.');
replace({
pathname: '/login',
state: { nextPathname: nextState.location.pathname }
});
}
}
};
Upvotes: 7