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.
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 }
});
}
}
};
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.
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});
});
}
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> )
}
}
Related
I am working on the React project which require authorization via AWS Cognito. By following the documentation, I can get the ID token successfully. However I don't know how to update the state in the onsuccess callback, here is my code. Appreciate your help.
class A extends Component{
constructor(){
super();
this.state = {
auth: "";
token: "";
login:false;
}
}
}
hanleSignIn = ()=>{
let auth = this.initCognitoSDK();
let curUrl = window.location.href;
auth.parseCognitoWebResponse(curUrl);
auth.getSession();
}
initCognitoSDK=()=>{
let authData = {
ClientId: // client id
AppWebDomain: // "https://" part.
TokenScopesArray: // like ['openid','email','phone']...
RedirectUriSignIn: 'http://localhost:3000',
RedirectUriSignOut: 'http://localhost:3000',
IdentityProvider: **,
UserPoolId: **,
AdvancedSecurityDataCollectionFlag: false
};
let auth = new CognitoAuth(authData);
auth.userhandler = {
onSuccess: function (result) {
if(result){
let idToken = result.getIdToken().getJwtToken();
//here I want to update the following state once get token successfully
// However I can't reach this.setState in the callback function
this.setState({
auth: auth,
token: idToken,
login: true
})
}
},
onFailure: function (err) {
console.log("Error!" + err);
}
};
auth.useCodeGrantFlow();
return auth;
}
You are using a regular function for your callback which means when the callback is called, the context of this is auth.userhandler which does not have a setState property. As with other parts of your code, you should use the arrow function notation so that it uses the lexical this which is your component instance:
auth.userhandler = {
onSuccess: (result) => {
...
this.setState({ ... });
}
};
There is a similar question here, the suggested answer doesn't seem to work for me.
Here is how I am using firebase mobile sign in for sign in ReactJS. I am also setting auth state persistence on sign in (see below code).
However when I refresh the page after sign in the user object disappears i.e "User is not signed in" message gets printed in componentDidMount.
What could I be doing wrong ?
class SignInScreen extends React.Component {
constructor(props) {
super(props);
this.state = {
isSignedIn: false,
};
}
// Configure FirebaseUI.
uiConfig = {
// Popup signin flow rather than redirect flow.
signInFlow: "popup",
signInOptions: [
{
provider: firebase.auth.PhoneAuthProvider.PROVIDER_ID,
defaultCountry: "US",
},
],
callbacks: {
// Avoid redirects after sign-in.
signInSuccessWithAuthResult: () => false,
},
};
// Listen to the Firebase Auth state and set the local state.
componentDidMount() {
this.unregisterAuthObserver = firebase.auth().onAuthStateChanged((user) => {
this.setState({ isSignedIn: !!user });
if (user != null) {
this.setAuthPersistence(); // Setting state persistence here
}
});
if(firebase.auth().currentUser){
console.log("User is already signed in")
}else{
console.log("User is not signed in")
}
}
setAuthPersistence = () => {
firebase
.auth()
.setPersistence(firebase.auth.Auth.Persistence.LOCAL)
.then(function() {
console.log("Local persistence set");
})
.catch(function(error) {
// Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
console.log("Local persistence has not been set");
});
};
// Make sure we un-register Firebase observers when the component unmounts.
componentWillUnmount() {
this.unregisterAuthObserver();
}
render() {
//Displaying firebase auth when user is not signed in
if (!this.state.isSignedIn) {
return (
<div>
<StyledFirebaseAuth
uiConfig={this.uiConfig}
firebaseAuth={firebase.auth()}
/>
</div>
);
}
return <Redirect to="/signedInUser" />;
}
}
export default SignInScreen;
Same as in the answer you linked, your if(firebase.auth().currentUser) runs before Firebase has asynchronously refreshed the authentication state, and thus before the user is signed in again.
Any code that needs to respond to the authentication state, needs to be in the onAuthStateChanged callback. So:
componentDidMount() {
this.unregisterAuthObserver = firebase.auth().onAuthStateChanged((user) => {
this.setState({ isSignedIn: !!user });
if (user != null) {
this.setAuthPersistence(); // Setting state persistence here
}
if(firebase.auth().currentUser){
console.log("User is already signed in")
}else{
console.log("User is not signed in")
}
});
}
I am currently able to get user data from the Firestore however I'm having trouble saving the users document data. I'm getting an error below in my console
TypeError: this.setState is not a function
at Object.next (RepRequest.js:32)
at index.cjs.js:1344
at index.cjs.js:1464
I attempted to follow another user's question from
Can't setState Firestore data, however still no success.
I do have a two api request right after getting the data and I am able to setState then. I tried incorporating the Firestore request in the promise.all but was unable to successfully, which is why I have it separated. Maybe I'm headed down the wrong path, any guidance is appreciated.
import React, { useEffect, useState } from "react";
import app from "./config/base.js";
import axios from "axios";
export default class RepRequest extends React.Component {
constructor(props) {
super(props);
this.state = {
userInfo: [],
fedSens: [],
fedReps: []
};
}
componentDidMount() {
const items = [];
app.auth().onAuthStateChanged(function(user) {
if (user) {
console.log("User is signed in");
let db = app
.firestore()
.collection("user")
.doc(user.uid);
db.get().then(doc => {
if (doc.exists) {
console.log("Document data:", doc.data());
items.push(doc.data());
} else {
console.log("No doc exists");
}
});
}
this.setState({ userInfo: items });
});
Promise.all([
axios.get(
`https://api.propublica.org/congress/v1/116/senate/members.json`,
{
headers: { "X-API-Key": "9wGKmWl3kNiiSqesJf74uGl0PtStbcP2mEzSvjxv" }
}
),
axios.get(
`https://api.propublica.org/congress/v1/116/house/members.json`,
{
headers: { "X-API-Key": "9wGKmWl3kNiiSqesJf74uGl0PtStbcP2mEzSvjxv" }
}
)
]).then(([rest1, rest2]) => {
this.setState({
fedSens: rest1,
fedReps: rest2
});
});
}
render() {
if (this.state.fedReps.length <= 0)
return (
<div>
<span>Loading...</span>
</div>
);
else {
console.log(this.state.fedReps);
return <div>test</div>;
}
}
}
Your problem arises from mixing lambda function declarations ((...) => { ... }) and traditional function declarations (function (...) { }).
A lambda function will inherit this from where it was defined but a traditional function's this will be isolated from the context of where it was defined. This is why it is common to see var self = this; in legacy-compatible code because this usually didn't match what you wanted it to.
Here is an example snippet demonstrating this behaviour:
function doSomething() {
var anon = function () {
console.log(this); // 'this' is independent of doSomething()
}
var lambda = () => {
console.log(this); // inherits doSomething's 'this'
}
lambda(); // logs the string "hello"
anon(); // logs the 'window' object
}
doSomething.call('hello')
Solution
So you have two approaches available. Use whichever you are comfortable with.
Option 1: Use a lambda expression
app.auth().onAuthStateChanged(function(user) {
to
app.auth().onAuthStateChanged((user) => {
Option 2: Assign a "self" variable
const items = [];
app.auth().onAuthStateChanged(function(user) {
// ...
this.setState({ userInfo: items });
}
to
const items = [];
const component = this; // using "component" makes more sense than "self" in this context
app.auth().onAuthStateChanged(function(user) {
// ...
component.setState({ userInfo: items });
}
My Problem
I am using React and Apollo in my frontend application. When I login as a regular user I want to be navigated to a certain path, but before that I need to set my providers state with my selectedCompany value. However, sometimes I get my value from my provider in my components CDM and sometimes I don't, so I have to refresh in order to get the value.
I've tried to solve this, but with no luck. So I am turning to the peeps at SO.
My setup (code)
In my login component I have my login mutation, which looks like this:
login = async (username, password) => {
const { client, qoplaStore, history } = this.props;
try {
const result = await client.mutate({
mutation: LOGIN,
variables: {
credentials: {
username,
password,
}}
});
const authenticated = result.data.login;
const { token, roles } = authenticated;
sessionStorage.setItem('jwtToken', token);
sessionStorage.setItem('lastContactWithBackend', moment().unix());
qoplaStore.setSelectedUser(authenticated);
qoplaStore.setUserSessionTTL(result.data.ttlTimeoutMs);
if (roles.includes(ROLE_SUPER_ADMIN)) {
history.push("/admin/companies");
} else {
// This is where I'm at
this.getAndSetSelectedCompany();
history.push("/admin/shops");
}
} catch (error) {
this.setState({ errorMessage: loginErrorMessage(error) });
}
};
So if my login is successful I check the users roles, and in this case, I will get into the else statement. The function getAndSetSelectedCompany look like this:
getAndSetSelectedCompany = () => {
const { client, selectedValues, qoplaStore } = this.props;
client.query({ query: GET_COMPANIES }).then(company => {
selectedValues.setSelectedCompany(company.data.getCompanies[0]);
});
};
So I am fetching my companies try to set one of them in my providers state with the function setSelectedCompany. selectedValues is what im passing down from my consumer to all my routes in my router file:
<QoplaConsumer>
{(selectedValues) => {
return (
...
)
}}
</QoplaConsumer
And in my provider I have my setSelectedCompany function which looks like this:
setSelectedCompany = company => {
this.persistToStorage(persistContants.SELECTED_COMPANY, company);
this.setState({
selectedCompany: company
})
};
And my selectedValues are coming from my providers state.
And in the component that has the route I'm sending the user to I have this in it's CDM:
async componentDidMount() {
const { client, selectedValues: { selectedCompany, authenticatedUser } } = this.props;
console.log('authenticatedUser', authenticatedUser)
if (selectedCompany.id === null) {
console.log('NULL')
}
Sometimes I get into the if statement, and sometimes I don't. But I rather always come into that if statement. And that is my current problem
All the help I can get is greatly appreciated and if more info is needed. Just let me know.
Thanks for reading.
Your getAndSetSelectedCompany is asynchronous and it also calls another method that does setState which is also async.
One way to do this would be to pass a callback to the getAndSetSelectedCompany that would be passed down and executed when the state is actually set.
changes to your login component
if (roles.includes(ROLE_SUPER_ADMIN)) {
history.push("/admin/companies");
} else {
// This is where I'm at
this.getAndSetSelectedCompany(()=>history.push("/admin/shops"));
}
changes to the two methods that are called
getAndSetSelectedCompany = (callback) => {
const { client, selectedValues, qoplaStore } = this.props;
client.query({ query: GET_COMPANIES }).then(company => {
selectedValues.setSelectedCompany(company.data.getCompanies[0], callback);
});
};
setSelectedCompany = (company, callback) => {
this.persistToStorage(persistContants.SELECTED_COMPANY, company);
this.setState({
selectedCompany: company
}, callback)
};
I'm working on an app with a login page and the rest of the pages of the app (should be logged in to view). I'm using react-boilerplate. From this example, I edited my asyncInjectors.js file to have redirectToLogin and redirectToDashboard methods:
//asyncInjectors.js
export function redirectToLogin(store) {
return (nextState, replaceState) => {
const isAuthenticated = store.getState().get('app').get('isAuthenticated');
if (!isAuthenticated) {
replaceState({
pathname: '/login',
state: {
nextPathname: nextState.location.pathname,
},
});
}
};
}
export function redirectToDashboard(store) {
return (nextState, replaceState) => {
const isAuthenticated = store.getState().get('app').get('isAuthenticated');
if (isAuthenticated) {
replaceState('/');
}
}
}
Then I just set the redirectToLogin as the onEnter of the pages and redirectToDashboard for the login page.
It works fine but when the page is refreshed (F5) when logged in, the login page renders briefly and then renders the actual page. The login page just dispatches an authenticate action in componentWillMount and then redirects in componentDidUpdate:
//login.js
componentWillMount() {
this.props.dispatch(authenticate());
}
componentDidUpdate(prevProps, prevState) {
if (this.props.isAuthenticated) {
const nextPathname = prevProps.location.state ? prevProps.location.state.nextPathname : '/';
browserHistory.push(nextPathname);
}
}
The container for the pages also has the same componentWillMount code. Not sure if it's because of the sagas but here's the code:
//sagas.js
export function* login({ user, password }) {
try {
const token = yield call(app.authenticate, {
strategy: 'local',
user,
password,
});
return token;
} catch (error) {
return onError(error);
}
}
// For page refresh after logging in
export function* authenticate() {
try {
const token = yield call(app.authenticate);
return token;
} catch (error) {
return onError(error);
}
}
export function* logout() {
try {
const response = yield call(app.logout);
return response;
} catch (error) {
return onError(error);
}
}
export function* loginFlow() {
while (true) {
const request = yield take(LOGIN_REQUEST);
const winner = yield race({
auth: call(login, request.data),
logout: take(LOGOUT_REQUEST),
});
if (winner.auth && winner.auth.accessToken) {
yield put(actions.setAuthState(true));
}
}
}
export function* logoutFlow() {
while (true) {
yield take(LOGOUT_REQUEST);
yield put(actions.setAuthState(false));
yield call(logout);
browserHistory.push('/login');
}
}
export function* authenticateFlow() {
while (true) {
yield take(AUTHENTICATE);
const response = yield call(authenticate);
if (response && response.accessToken) {
yield put(actions.setAuthState(true));
}
}
}
export default [
loginFlow,
logoutFlow,
authenticateFlow,
];
How do I get rid of the flashing login page?
EDIT:
When I tried gouroujo's answer, I couldn't logout.
//asyncInjectors.js
import jwtDecode from 'jwt-decode';
export function redirectToLogin(store) {
return (nextState, replaceState, callback) => {
const token = localStorage.token;
if (token) {
const jwt = jwtDecode(token);
if (jwt.exp <= (new Date().getTime() / 1000)) {
store.dispatch(actions.setAuthState(false));
replaceState({
pathname: '/login',
state: {
nextPathname: nextState.location.pathname,
},
});
}
}
store.dispatch(actions.setAuthState(true));
callback();
};
}
When I hit refresh, the login page doesn't show but now I can't log out.
You have two way to avoid flashing the login page on initial render : make your authenticate function synced or wait the answer with a loading page.
1- Check if your token is present and valid (expiration date) client-side to choose if you have to redirect the user to the login or the dashboard page first. When the answer come back from your server you correct your initial guess but in the vaste majority you won't need to.
user landing ->
check the token client-side -> redirect to login if needed
check the token server-side -> wait answer -> re-redirect if needed
To check the token client-side you have to check the local storage. For example:
class App extends Component {
requireAuth = (nextState, replace) => {
if (!localStorage.token) {
replace({
pathname: '/login',
state: { nextPathname: nextState.location.pathname }
})
}
}
render() {
return (
<Router history={browserHistory}>
<Route path="/login" component={LoginPage} />
<Route
path="/"
component={AppLayout}
onEnter={this.requireAuth}
> ... </Route>
)
}
}
If you use a token with a relatively short expiration date, you will also have to check the expiration date, thus decode the token.
try {
const jwt = JWTdecode(token);
if (moment().isBefore(moment.unix(jwt.exp))) {
return nextState;
} else {
replace({
pathname: '/login',
state: { nextPathname: nextState.location.pathname }
})
}
} catch (e) {
replace({
pathname: '/login',
state: { nextPathname: nextState.location.pathname }
})
}
2- Show a loading screen before you receive the answer from the server. add some Css transition effect on opacity to avoid the "flash"