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
Related
I want to run getUser function every time the user goes to some other link.
The following is my getUser function
const getUser = async () => {
if (localStorage.getItem('access') === null || localStorage.getItem('refresh') === null || localStorage.getItem('user') === null) {
setUser({ email: null });
setIsLoggedIn(false);
return;
}
const responseForAccessToken = await verifyTokenAPI(localStorage.getItem('access'));
console.log(responseForAccessToken);
if (responseForAccessToken.status >= 400) {
const newAccessTokenResponse = await getAccessTokenAPI(localStorage.getItem('refresh'));
console.log(newAccessTokenResponse);
if (newAccessTokenResponse.status >= 400) {
localStorage.removeItem('access');
localStorage.removeItem('refresh');
localStorage.removeItem('user');
setUser({ email: null });
setIsLoggedIn(false);
return;
}
localStorage.removeItem('access');
localStorage.setItem('access', newAccessTokenResponse.access);
}
I want to verify token every time the user changes routes. Therefore, I used getUser function in useEffect in my App.js
const history = useHistory();
const { getUser } = useAuth();
useEffect(() => {
history.listen((location) => {
console.log(`You changed the page to: ${location.pathname}`);
});
getUser();
}, [history]);
Every time I change routes the useEffect runs and console logs the message but does not run getUser function.
I am using Link from react-router-dom
<h1>Welcome {user.email}</h1>
<Link to="/protected-route-2">Protected Route 2</Link>
<button
onClick={() => logout({ callBack: () => history.push("/login") })}
>
Logout
</button>
Additionally, I also have a PrivateRoute component
const Privateroute = ({ component: Component, ...rest }) => {
const { isLoggedIn, getUser } = useAuth()
console.log(isLoggedIn);
const location = useLocation()
if (isLoggedIn) return <Route {...rest} render={props => <Component {...props} />} />;
return <Redirect to={{ pathname: '/login', state: { from: location.pathname } }} />
}
I am not sure if I am doing things right. Can someone suggest another approach to this problem? Any suggestion will be appreciated. Thanks in advance.
You should use the useLocation hook (as shown in the documentation) instead of the useHistory, which would give you the current location and use that as the dependency for the useEffect:
const location = useLocation();
const { getUser } = useAuth();
useEffect(() => {
console.log(`You changed the page to: ${location.pathname}`);
getUser();
}, [location]);
In your code, the history object does not change and the effect is only fired once, the reason you keep getting the console logs when the location changes is that you added a listener to the history.
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;
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.
I have a react routing problem I cannot figure out. I have authenticated routes to everything except my login page. The authentication is working correctly and I can navigate around the page when clicking buttons and links but when I type in the route directly to the URL I am redirected to localhost:3000/ . I know why this is happening I just cannot think of a fix for it. Below is all related code. The main problem is the useEffect hook in the login component redirecting to / .
When I type in the url something like localhost:3000/user I am sent to localhost:3000/
What can I do instead?
PrivateRoute.js
const PrivateRoute = ({ component: Component, ...rest }) => {
const authContext = useContext(AuthContext);
const { isAuthenticated } = authContext;
return (
<Route
{...rest}
render={props =>
!isAuthenticated ? <Redirect to='/login' /> : <Component {...props} />
}
/>
);
};
Login.js
const Login = props => {
useEffect(() => {
if (isAuthenticated) {
props.history.push('/');
}
}, [error, onLoad, isAuthenticated, props.history]);
useEffect(() => {
onLoad();
}, [error, onLoad, isAuthenticated, props.history]);
}
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/>