Authentication in React - reactjs

When the user successfully logs in, the user must be re-directed into another page. This page is not accessible if you are not logged in.
Here is the login form code
class LoginForm extends React.Component {
handleFormSubmit = e => {
e.preventDefault();
const user = e.target.elements.user.value;
const password = e.target.elements.password.value;
this.props.onAuth(user, password);
};
componentDidUpdate() {
console.log(this.props.successLogin)
};
Here is the mapStateToProps and mapDispatchToProps function
const mapStateToProps = (state) => {
return {
error: state.error,
successLogin: state.token
}
};
mapDispatchToProps = dispatch => {
return {
onAuth: (username, password) => dispatch(actions.authLogin(username, password))
}
};
And here is its action
export const authSuccess = token => {
return {
type: actionTypes.AUTH_SUCCESS,
token: token
}
};
Here is the code in the login page
const PrivateRoute = ({component: Component, ...rest}) => (
<Route {...rest} render={(props) => (
rest.auth
? <Component {...props}/>
:
<Redirect to='/'/>
)}/>
);
Here is the routing link
<PrivateRoute path="/Main_Page" exact component={Main_Page} auth={this.props.isAuthenticated}/>
And its connect
const mapStateToProps = state => {
return {
isAuthenticated: state.token !== null
};
};
The console.log prints twice. ones null and ones with the actual token.
My question is what is wrong in the logic that is making the console.log(this.props.successLogin) print ones null then the token, and vice versa when logging out ?
The challenge this poses is my page ends up re-directed to <Redirect to='/'/>``, although I am authenticated, and I can access all thePrivateRoute path`

You will have to check if user successfully authenticated or nor before rendering
{this.props.isAuthenticated && <PrivateRoute path="/Main_Page" exact component={Main_Page} auth={this.props.isAuthenticated}/>}

Here is how I fixed it
<Route exact path="/"render={() => (this.props.isAuthenticated ? (
<Redirect to="/Main_Page"/>) : (
<Log_in_Page/>

Related

How can I dynamically generate a PrivateRoute redirect based on an axios response?

I have an endpoint in my app called /get-redirect, which basically redirects you to wherever you need to be.
E.g. if you're not logged in, the response from this endpoint is an HTTP 200 with the redirect_location field in the response set to mysite.com/login. If you're logged in, but haven't completed step 2 of onboarding, it sends an HTTP 200 with redirect_location set to mysite.com/step2, etc.
I want to use PrivateRoute in React Router for authenticated pages, and I want any redirects to go to the result of the /get-redirect endpoint. I would then render the appropriate component in the statement.
This is what I have so far but I keep getting that the getPage() function is returning undefined. What am I missing here?
const fakeAuth = {
getPage(cb) {
if (document.cookie === null) {
return '/login'
}
const url = "https://api.mysite.com/get-redirect"
axios.get(url)
.then(function (response) {
return response.redirect_location
}).catch(function (error) {
if (error.response.status === 401) {
return '/401'
}
return '/404'
})
},
}
function PrivateRoute({ children, ...rest }) {
return (
<Route {...rest} render={() => {
return <Redirect to={{
pathname: fakeAuth.getPage()
}} />
}} />
)
}
class App extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<Router>
<Switch>
...
<PrivateRoute path="/dashboard">
<Dashboard />
</PrivateRoute>
....
Issue
getPage needs to return the Promise chain started by axios.
You will need to wait for the Promise to resolve (or reject) in the PrivateRoute component but you can't just return the result as the redirect target.
Solution
I suggest abstracting a component to do the endpoint check and conditionally render the redirect or the original children prop. While the endpoint is checked conditionally render some loading state or null until the expected result is returned then conditionally render the redirect or child Dashboard component.
Example
const CheckRedirect = ({ children }) => {
const [isChecking, setIsChecking] = React.useState(true);
const [target, setTarget] = React.useState("/");
React.useEffect(() => {
fakeAuth
.getPage()
.then((target) => {
setTarget(target);
})
.finally(() => setIsChecking(false));
}, []);
if (isChecking) return "...Checking"; // or loading spinner, or null
return target.redirect_location ? (
<Redirect to={target.redirect_location} />
) : (
children
);
};
function PrivateRoute({ children, ...rest }) {
return (
<Route {...rest} render={() => <CheckRedirect children={children} />} />
);
}
Demo
For demo this is the mocked auth getPage component, it has a 50% chance to return redirect target.
const fakeAuth = {
getPage(cb) {
return new Promise((resolve) => {
setTimeout(() => {
return resolve(
Math.random() < 0.5 ? { redirect_location: "/step2" } : {}
);
}, 3000);
});
}
};

Warning: Cannot update a component while rendering a different component. ReactRedux.Consumer

I am getting a warning in my web app and have read a lot of posts about the issue. Unfortunately I have not managed to resolve my problem and hope someone might have some advice. From what I can tell I need to find a way of dispatching to the store in a useEffect. But my efforts so far have been unsuccessful.
The warning says:
index.js:1 Warning: Cannot update a component (Connect(TabMenuComponent)) while rendering a different component (ReactRedux.Consumer). To locate the bad setState() call inside ReactRedux.Consumer, follow the stack trace as described in https://reactjs.org/link/setstate-in-render
The stack trace further points me to this file. It points to line 30 which is the store.dispatch line:
export const AuthRoute = ({ component: Component, roles, computedMatch, ignoreRedirection, ...rest }) => (
<Route exact {...rest} render={props => {
return <ReactReduxContext.Consumer>
{({ store }) => {
const user = store.getState().appData.user;
if (!user) {
auth.setRedirectUrl(window.location.pathname);
return <Redirect to={auth.loginUrl} />;
}
const redirectUrl = auth.getRedirectUrl();
if (redirectUrl && !ignoreRedirection) {
auth.removeRedirectUrl();
return <Redirect to={redirectUrl} />;
}
if (roles && roles.length && !roles.some(neededRole => user.roles.some(userRole => userRole === neededRole))) {
return <BaseLayout authError={stringKeys.error.unauthorized}></BaseLayout>;
}
store.dispatch({ type: "ROUTE_CHANGED", url: computedMatch.url, path: computedMatch.path, params: computedMatch.params })
return <Component {...props} />;
}}
</ReactReduxContext.Consumer>;
}
} />
);
You are dispatching an action in the middle of a render which is not correct. What you should instead do is to create an HOC or a wrapper component to your input Component and dispatch the action once thee component is mounted
With class component wrapper:
class CompWithDispatch extends React.Component {
componentDidMount() {
const { store, type, url, path, params } = this.props;
store.dispatch({ type, url, path, params })
}
render() {
const { store, type, url, path, params , component: Component, ...rest} = this.props;
return <Component {...rest} />
}
}
With function component wrapper
const CompWithDispatch = (props) => {
const { store, type, url, path, params, component:Component, ...rest } = props;
useEffect(() => {
store.dispatch({ type, url, path, params })
}, []);
return <Component {...rest} />
}
and use it like
export const AuthRoute = ({ component, roles, computedMatch, ignoreRedirection, ...rest }) => (
<Route exact {...rest} render={props => {
return <ReactReduxContext.Consumer>
{({ store }) => {
const user = store.getState().appData.user;
if (!user) {
auth.setRedirectUrl(window.location.pathname);
return <Redirect to={auth.loginUrl} />;
}
const redirectUrl = auth.getRedirectUrl();
if (redirectUrl && !ignoreRedirection) {
auth.removeRedirectUrl();
return <Redirect to={redirectUrl} />;
}
if (roles && roles.length && !roles.some(neededRole => user.roles.some(userRole => userRole === neededRole))) {
return <BaseLayout authError={stringKeys.error.unauthorized}></BaseLayout>;
}
const additionalProps = { type: "ROUTE_CHANGED", url: computedMatch.url, path: computedMatch.path, params: computedMatch.params })
return <CompWithDispatch {...additionalProps} {...props} component={component}/>;
}}
</ReactReduxContext.Consumer>;
}
} />
);

How to keep authenticated state on refresh?

I'm using firebase authentication for my app. I used useAuth hook from here. Integrate with react-router guide about redirect (Auth).
SignIn,SignOut function is working as expected. But when I try to refresh the page. It redirects to /login again.
My expected: Redirect to / route when authenticated.
I tried to add this code in PrivateRoute.js
if (auth.loading) {
return <div>authenticating...</div>;
}
So I can refresh the page without redirect to /login but it only show authenticating... when click the log out button.
Here is my code: https://codesandbox.io/s/frosty-jennings-j1m1f?file=/src/PrivateRoute.js
What I missed? Thanks!
Issue
Seems you weren't rendering the "authenticating" loading state quite enough.
I think namely you weren't clearing the loading state correctly in the useEffect in useAuth when the initial auth check was resolving.
Solution
Set loading true whenever initiating an auth check or action, and clear when the check or action completes.
useAuth
function useProvideAuth() {
const [loading, setLoading] = useState(true); // <-- initial true for initial mount render
const [user, setUser] = useState(null);
// Wrap any Firebase methods we want to use making sure ...
// ... to save the user to state.
const signin = (email, password) => {
setLoading(true); // <-- loading true when signing in
return firebase
.auth()
.signInWithEmailAndPassword(email, password)
.then((response) => {
setUser(response.user);
return response.user;
})
.finally(() => setLoading(false)); // <-- clear
};
const signout = () => {
setLoading(true); // <-- loading true when signing out
return firebase
.auth()
.signOut()
.then(() => {
setUser(false);
})
.finally(() => setLoading(false)); // <-- clear
};
// Subscribe to user on mount
// Because this sets state in the callback it will cause any ...
// ... component that utilizes this hook to re-render with the ...
// ... latest auth object.
useEffect(() => {
const unsubscribe = firebase.auth().onAuthStateChanged((user) => {
if (user) {
setUser(user);
} else {
setUser(false);
}
setLoading(false); // <-- clear
});
// Cleanup subscription on unmount
return () => unsubscribe();
}, []);
// Return the user object and auth methods
return {
loading,
user,
signin,
signout
};
}
Check the loading state in PrivateRoute as you were
function PrivateRoute({ children, ...rest }) {
const auth = useAuth();
if (auth.loading) return "authenticating";
return (
<Route
{...rest}
render={({ location }) =>
auth.user ? (
children
) : (
<Redirect
to={{
pathname: "/login",
state: { from: location }
}}
/>
)
}
/>
);
}
Demo
Try this approach, it works for me :
const mapStateToProps = state => ({
...state
});
function ConnectedApp() {
const [auth, profile] = useAuth()
const [isLoggedIn, setIsLoggedIn] = useState(false)
useEffect(() => {
if (auth && auth.uid) {
setIsLoggedIn(true)
} else {
setIsLoggedIn(false)
}
}, [auth, profile]);
return (<Router>
<Redirect to="/app/home"/>
<div className="App">
<Switch>
<Route path="/home"><Home/></Route>
<Route path="/login"><Login styles={currentStyles}/></Route>
<Route path="/logout"><Logout styles={currentStyles}/></Route>
<Route path="/signup" render={isLoggedIn
? () => <Redirect to="/app/home"/>
: () => <Signup styles={currentStyles}/>}/>
<Route path="/profile" render={isLoggedIn
? () => <Profile styles={currentStyles}/>
: () => <Redirect to="/login"/>}/>
</Switch>
</div>
</Router>);
}
const App = connect(mapStateToProps)(ConnectedApp)
export default App;

React flashing Private Route when User is not authenticated

I'm using react-router-dom to secure the entire application. All routes are protected under a ProtectedRoute component (see code below), which redirects to an external url, a single-sign-on (SSO) page if the user is not logged in.
Problem:
When the user goes to '/home', they get a brief glimpse (a "flash") of the protected route before getting redirected to 'external-login-page.com/' (the login page). How do I avoid the flashing so that the user only sees the login page?
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
isAuthenticated,
...rest
}) => {
if (!isAuthenticated) { // redirect if not logged in
return (
<Route
component={() => {
window.location.href = 'http://external-login-page.com/';
return null;
}}
/>
);
} else {
return <Route {...rest} />;
}
};
window.location.href can be called earlier to prevent flashing. Also in your specific case what you probably want is to render nothing at all when the user is not authenticated.
The code may look like this:
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
isAuthenticated,
...rest
}) => {
if (!isAuthenticated) { // redirect if not logged in
window.location.href = 'http://external-login-page.com/';
return null;
} else {
return <Route {...rest} />;
}
};
You might consider the Redirect component
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
isAuthenticated,
...rest
}) => {
if (!isAuthenticated) {
return <Redirect to='https://external-login-page.com/' />
} else {
return <Route {...rest} />;
}
};
I would guess that invoking window directly + return null is rendering the React app for a split second before the page reloads.
You can use the Redirect component in a simpler way like this.
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
isAuthenticated,
children,
...rest
}) => {
return <Route {...rest} render={() => isAuthenticated ? children : <Redirect to='http://external-login-page.com/' />}
}
Posting the solution that eventually worked for me: instead of blocking by Router, block by App.
The key is to split your App into two components, AuthenticatedApp and UnauthenticatedApp. From there, lazy load the correct component depending on the user's level of access. This way, if they're not authorized, their browser won't even load AuthenticatedApp at all.
AuthenticatedApp is a component to your entire app, providers, routers, etc. Whatever you had in App.tsx originally should go here.
UnauthenticatedApp is a component that you want your users to see when they're not allowed to access the application. Something like "Not authorized. Please contact admin for help."
App.tsx
const AuthenticatedApp = React.lazy(() => import('./AuthenticatedApp'));
const UnauthenticatedApp = React.lazy(() => import('./UnauthenticatedApp'));
// Dummy function to check if user is authenticated
const sleep = (time) => new Promise((resolve) => setTimeout(resolve, time));
const getUser = () => sleep(3000).then(() => ({ user: '' }));
const App: React.FC = () => {
// You should probably use a custom `AuthContext` instead of useState,
// but I kept this for simplicity.
const [user, setUser] = React.useState<{ user: string }>({ user: '' });
React.useEffect(() => {
async function checkIfUserIsLoggedInAndHasPermissions() {
let user;
try {
const response = await getUser();
user = response.user;
console.log(user);
setUser({ user });
} catch (e) {
console.log('Error fetching user.');
user = { user: '' };
throw new Error('Error authenticating user.');
}
}
checkIfUserIsLoggedInAndHasPermissions();
}, []);
return (
<React.Suspense fallback={<FullPageSpinner />}>
{user.user !== '' ? <AuthenticatedApp /> : <UnauthenticatedApp />}
</React.Suspense>
);
};
export default App;
Read Kent C Dodd's great post about it here [0]!
EDIT: Found another great example with a similar approach, a bit more complex - [1]
[0] https://kentcdodds.com/blog/authentication-in-react-applications?ck_subscriber_id=962237771
[1] https://github.com/chenkie/orbit/blob/master/orbit-app/src/App.js

Protected route is redirecting before API response arrives

I am using Protected Route to redirect a user to subscription page if the subscription is ended. But Route is not waiting for the api response to arrive.
const ProtectedRoute = ({ component: Component, ...rest }) => {
const { subscriptionData = {} } = useContext(AppContext) || {};
return (
<Route
render={(props) => {
if (subscriptionData && subscriptionData.active) {
return <Component {...props} />;
}
return <Redirect to={`${ROUTE_PREFIX}/settings/billing`} />;
}}
{...rest}
/>
);
};
API call is going in action -
export const getSubscription = () => async (dispatch) => {
const url = `/subscription/`;
await handleAjaxCalls(dispatch, 'GET', url, null, GET_SUBSCRIPTION);
};
The API call is mader from useEffect() of App.js file.
When the API call is going on subscriptionData is empty {} the redirection is happening to ${ROUTE_PREFIX}/settings/billing page. Any help to restrict this will be much appreciated.

Resources