I have implemented Firebase Auth (Sign In With Google) in a MERN stack app.
The /admin client-side route is a protected route. After I log in, I see the displayName of the logged in user, as shown below:
Admin.js
import React, { useContext } from "react";
import { Link } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import { getAuth, signOut } from "firebase/auth";
import { useFetchAllPosts } from "../hooks/postsHooks";
import Spinner from "../sharedUi/Spinner";
import { PencilAltIcon } from "#heroicons/react/outline";
import { UserContext } from "../context/UserProvider";
const Admin = () => {
const { data: posts, error, isLoading, isError } = useFetchAllPosts();
const auth = getAuth();
const navigate = useNavigate();
const { name } = useContext(UserContext);
const handleLogout = () => {
signOut(auth).then(() => {
navigate("/");
});
};
return (
<>
<h2>Hello {name}</h2>
<button className="border" onClick={handleLogout}>
Logout
</button>
<div>
{isLoading ? (
<Spinner />
) : isError ? (
<p>{error.message}</p>
) : (
posts.map((post) => (
<div
key={post._id}
className="flex w-80 justify-between px-6 py-2 border rounded mb-4 m-auto"
>
<Link to={`/posts/${post._id}`}>
<h2>{post.title}</h2>
</Link>
<Link to={`/posts/edit/${post._id}`}>
<PencilAltIcon className="w-5 h-5" />
</Link>
</div>
))
)}
</div>
</>
);
};
export default Admin;
App.js
import React from "react";
import { Routes, Route } from "react-router-dom";
import Home from "./components/screens/Home";
import PostCreate from "./components/screens/PostCreate";
import SinglePost from "./components/screens/SinglePost";
import Admin from "./components/screens/Admin";
import PostEdit from "./components/screens/PostEdit";
import Login from "./components/screens/Login";
import PrivateRoute from "./components/screens/PrivateRoute";
const App = () => {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/admin" element={<PrivateRoute />}>
<Route path="/admin" element={<Admin />} />
</Route>
<Route path="/posts/create" element={<PostCreate />} />
<Route path="/posts/:id" element={<SinglePost />} />
<Route path="/posts/edit/:id" element={<PostEdit />} />
<Route path="/login" element={<Login />} />
</Routes>
);
};
export default App;
PrivateRoute.js
import React from "react";
import { Navigate, Outlet, useLocation } from "react-router-dom";
import { getAuth } from "firebase/auth";
const PrivateRoute = () => {
const location = useLocation();
const auth = getAuth();
console.log(auth.currentUser);
return auth.currentUser ? (
<Outlet />
) : (
<Navigate to="/login" state={{ from: location }} />
);
};
export default PrivateRoute;
Now, if I refresh this page, I go back to the /login route.
However, this should not happen, because if I go to the root / route, I see the displayName of the current user.
If I refresh the page while on the root / route, I still see the displayName of the current user.
So, my question is why am I getting redirected to the /login page after I refresh the page on the /admin route? I am logged in, so I should remain on the Admin page.
The logic of whether a user is logged-in or not is implemented in the UserProvider component:
UserProvider.js
import React, { useState, useEffect, createContext } from "react";
import { getAuth, onAuthStateChanged } from "firebase/auth";
export const UserContext = createContext();
const UserProvider = ({ children }) => {
const [name, setName] = useState(null);
const auth = getAuth();
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
if (user) {
console.log(user);
setName(user.displayName);
} else {
setName(null);
}
});
return unsubscribe;
}, [auth]);
const user = {
name,
setName,
};
return <UserContext.Provider value={user}>{children}</UserContext.Provider>;
};
export default UserProvider;
The Firebase Authentication SDK automatically restores the user upon reloading the page/app. This does require a call to the server - a.o. to check if the account hasn't been disabled. While that verification is happening, auth.currentUser will be null.
Once the user account has been restored, the auth.currentUser will get a value, and (regardless of whether the restore succeeded or failed) any onAuthStateChanged listeners are called.
What this means is that you should not check the auth.currentUser value in code that runs immediately on page load, but should instead react to auth state changes with a listener.
If you need to route the user based on their authentication state, that should happen in response to the auth state change listener too. If you want to improve on the temporary delay that you get from this, you can consider implementing this trick that Firebaser Michael Bleigh talked about at I/O a couple of years ago: https://www.youtube.com/watch?v=NwY6jkohseg&t=1311s
I figured out a solution. I installed the react-firebase-hooks npm module and used the useAuthState hook to monitor the authentication status.
PrivateRoute.js
import React from "react";
import { Navigate, Outlet, useLocation } from "react-router-dom";
import { getAuth } from "firebase/auth";
import { useAuthState } from "react-firebase-hooks/auth";
const PrivateRoute = () => {
const location = useLocation();
const auth = getAuth();
const [user, loading] = useAuthState(auth);
if (loading) {
return "Loading...";
}
return user ? (
<Outlet />
) : (
<Navigate to="/login" state={{ from: location }} />
);
};
export default PrivateRoute;
Related
I'm new to ReactJS and am building a basic application. Here I'm using protected router and implementing an authorization mechanism with useContext and local storage. The aim is to redirect users that are not logged in to the Login Page if they attempt to access Dashboard.
After I do log in, the access token is saved to local storage and account info is saved in auth context. Then I go to Dashboard and I reload the page. I implemented a useEffect hook to check for token in local storage and I thought that when I reload at the Dashboard page, the auth provider would check for the token and return a result that I'm authenticated. However it doesn't work as expected so I am redirected to Login page (Although the useEffect callback was triggered)
Below is my code:
src\components\App\index.js
import { Routes, Route } from 'react-router-dom';
import Login from '../Login';
import Signup from '../Signup';
import GlobalStyles from '../GlobalStyles';
import ThemeProvider from 'react-bootstrap/ThemeProvider';
import RequireAuth from '../RequireAuth';
import Layout from '../Layout';
import Dashboard from '../Dashboard';
import Account from '../Account';
function App() {
return (
<GlobalStyles>
<ThemeProvider>
<Routes>
<Route path="/" element={<Login />} />
<Route path="/login" element={<Login />} />
<Route path="/signup" element={<Signup />} />
<Route element={<RequireAuth />}>
<Route path="/" element={<Layout />}>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/account" element={<Account />} />
</Route>
</Route>
</Routes>
</ThemeProvider>
</GlobalStyles>
);
}
export default App;
src\components\RequireAuth\index.js
import { useLocation, Navigate, Outlet } from 'react-router-dom';
import useAuth from '../../hooks/useAuth';
function RequireAuth() {
const { auth } = useAuth();
const location = useLocation();
return auth?.user ? (
<Outlet />
) : (
<Navigate to={{ pathname: '/', state: { from: location } }} replace />
);
}
export default RequireAuth;
src\hooks\useAuth.js
import { useContext } from 'react';
import { AuthContext } from '../context/AuthProvider';
function useAuth() {
return useContext(AuthContext);
}
export default useAuth;
src\context\AuthProvider.js
import { useEffect } from 'react';
import { createContext, useState } from 'react';
import api from '../helper/api';
const AuthContext = createContext({});
function AuthProvider({ children }) {
const [auth, setAuth] = useState({});
useEffect(() => {
const apiHelper = new api();
apiHelper.getAccountInfo().then((response) => {
setAuth(response.data);
});
}, []);
console.log(auth.user);
return (
<AuthContext.Provider value={{ auth, setAuth }}>
{children}
</AuthContext.Provider>
);
}
export { AuthContext, AuthProvider };
src\components\Login\index.js
import { useState, useRef, useEffect } from 'react';
import clsx from 'clsx';
import { Form, FormGroup, Button } from 'react-bootstrap';
import Alert from 'react-bootstrap/Alert';
import FloatingLabel from 'react-bootstrap/FloatingLabel';
import { useNavigate } from 'react-router';
import styles from './style.module.scss';
import logo from '../../assets/images/logo.png';
import LoadingSpinner from '../LoadingSpinner';
import api from '../../helper/api';
import useAuth from '../../hooks/useAuth';
const Login = () => {
const usernameRef = useRef();
const errorRef = useRef();
const [state, setState] = useState({
username: '',
password: ''
});
const { username, password } = state;
const [error, setError] = useState();
const [loading, setLoading] = useState(false);
useEffect(() => {
usernameRef.current.focus();
}, []);
const { setAuth } = useAuth();
const navigate = useNavigate();
const submitForm = (event) => {
event.preventDefault();
setLoading(true);
const apiHelper = new api();
apiHelper
.login({
username,
password
})
.then((response) => {
setLoading(false);
setError();
setAuth({ user: response.data.user });
localStorage.setItem('token', response.data.token);
navigate('/dashboard');
})
.catch((error) => {
setLoading(false);
setError(error.response.data.message);
usernameRef.current.focus();
});
};
return (
/** Some hmtml */
);
};
export default Login;
A video on how the error occurs: https://streamable.com/b1cp1t
Can anyone tell me where I'm wrong and how to fix it? Many thanks in advance!
you can think about a <Route> kind of like an if statement, if its path matches the current URL, it renders its element!
since the path of your first route in the list is "/" to the login page matches the need of the router it will redirect you there.
so, if you will delete this line:
<Route path="/" element={<Login />} />
and let the <RequireAuth /> take care of the path="/" it will check first if the user is logged in, if so let him continue.
if not it will redirect to "/login"
After successful login how can I see/access Dashboard, CreatedLink components mentioned inside the protected route. Could someone please advise how do I check
loginEmail is available in localStorage then display the above components else display login. Do I need another component for navigation to achieve this ?
App.js
import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom';
import Dashboard from "./components/dashboard";
import CreateLink from "./components/createLink";
import Nominate from "./components/nominate";
import Login from "./components/login";
import { ProtectedRoute } from "./components/protectedRoute";
import ErrorPage from "./components/errorPage";
function App() {
return (
<Router>
<div>
<div className="navbar-nav">
</div>
<Switch>
<ProtectedRoute exact path='/dashboard' component={Dashboard} />
<ProtectedRoute exact path='/createLink' component={CreateLink} />
<Route exact path='/' component={Login} />
<Route path='/nominate/:token' component={Nominate} />
<Route path='/errorPage' component={ErrorPage} />
</Switch>
</div>
</Router>
);
}
export default App;
login.js
import React, { useRef, useEffect, useState } from "react";
import { useGoogleLogin } from 'react-google-login';
import { refreshToken } from '../utils/refreshToken';
const clientId ="client_id_here";
const Login = () => {
const [userEmail, setUserEmail] = useState("");
const onSuccess = (res) =>{
console.log("Login successfully",res.profileObj);
const email = res.profileObj.email;
setUserEmail(email);
window.localStorage.setItem("loginEmail", email);
refreshToken(res);
}
const onFailure = (res) => {
console.log('Login failed: res:', res);
alert(
`Failed to login !`
);
};
const {signIn} = useGoogleLogin ({
onSuccess,
onFailure,
clientId,
isSignedIn: true,
accessType: 'offline',
})
return (
<div className="App">
<h1>Login</h1>
<div className="inputForm">
<button onClick={signIn}>
<img src="images/google.png" className="loginG"/>
<span className="loginText">Sign in</span>
</button>
</div>
</div>
)
}
export default Login;
protectedRoute.js
import React from "react";
import { Route, Redirect } from "react-router-dom";
export const ProtectedRoute = ({ component: Component, ...rest }) => {
return (
<Route
{...rest}
render={(props) => {
if (localStorage.getItem("loginEmail")) {
return <Component {...props} />;
} else {
return (
<>
<Redirect
to={{
pathname: "/",
state: {
from: props.location,
},
}}
/>
</>
);
}
}}
/>
);
};
Sandboxlink
https://codesandbox.io/s/gracious-forest-4csz3?file=/src/App.js
If I understand your question correctly, you want to be routed back to the protected route originally being accessed before getting bounced to the login route.
The protected route passed a referrer, i.e. from value in route state, you just need to access this value and imperatively redirect back to the original route.
login
Access the location.state and history objects to get the referrer value and redirect to it. This destructures from from location.state object and provides a fallback to the "/dashboard" route if the referrer is undefined (i.e. user navigated directly to "/login").
import { useHistory, useLocation } from 'react-router-dom';
const Login = () => {
const history = useHistory();
const { state: { from = "/dashboard" } = {} } = useLocation();
...
const onSuccess = (res) => {
console.log("Login successfully", res.profileObj);
const email = res.profileObj.email;
setUserEmail(email);
window.localStorage.setItem("loginEmail", email);
// refreshToken(res);
history.replace(from);
};
I also think a small refactor of your PrivateRoute component will allow you to first check localStorage when landing on a protected route.
export const ProtectedRoute = (props) => {
return localStorage.getItem("loginEmail") ? (
<Route {...props} />
) : (
<Redirect
to={{
pathname: "/",
state: {
from: props.location
}
}}
/>
);
};
When the users logs in using his/her credentials, the name of the user is displayed on the Header component. The user can log out using the logout link.
When the user clicks on the logout link, I remove the loggedInUser object saved in local storage. Then, I direct the user to the /login route, where I show the login form to the user.
When I use history.push("/login"), and click on the logout link, nothing happens. The loggedInUser object does not get removed from the local storage, and I am not directed to the login route. However, if I use window.location = "/login", everything works as expected.
Why is hitory.push("/login") not working as expected?
Header.js:
import React from "react";
import { Link, withRouter } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import { logout } from "./stateSlices/loginSlice";
const Header = ({ history }) => {
const { loggedInUser } = useSelector((state) => state.login);
const dispatch = useDispatch();
const logoutSubmitHandler = () => {
dispatch(logout());
localStorage.removeItem("loggedInUser");
window.location = "/login"; // THIS works
// history.push("/login"); // THIS does not work
};
return (
<header>
<nav>
<ul className="navbar-list">
{loggedInUser ? (
<div className="dropdown">
<button
className="btn btn-lg btn-primary dropdown-toggle"
type="button"
id="dropdownMenu2"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>
{loggedInUser.firstName}
</button>
<div
className="dropdown-menu dropdown-menu-right"
aria-labelledby="dropdownMenu2"
>
<button
className="dropdown-item"
type="button"
onClick={logoutSubmitHandler}
>
Logout
</button>
</div>
</div>
) : (
<Link to="/login" className="navbar-list-item">
Register/Login
</Link>
)}
</ul>
</nav>
</header>
);
};
export default withRouter(Header);
App.js:
import React from "react";
import Header from "./components/Header";
import { Route, Switch } from "react-router-dom";
import LoginForm from "./components/LoginForm";
import RegisterForm from "./components/RegisterForm";
import Welcome from "./components/Welcome";
import PasswordResetFormEmail from "./components/PasswordResetFormEmail";
import PasswordResetFormPassword from "./components/PasswordResetFormPassword";
const App = () => {
return (
<>
<Header />
<Switch>
<Route
path="/password/reset/:token"
component={PasswordResetFormPassword}
/>
<Route
path="/account/password/forgot"
component={PasswordResetFormEmail}
/>
<Route path="/register" component={RegisterForm} />
<Route path="/login" component={LoginForm} />
<Route path="/welcome" component={Welcome} />
</Switch>
</>
);
};
export default App;
index.js:
import React from "react";
import ReactDOM from "react-dom";
import "bootstrap/dist/css/bootstrap.min.css";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { BrowserRouter as Router } from "react-router-dom";
import { Provider } from "react-redux";
import store from "./store";
ReactDOM.render(
<Provider store={store}>
<Router>
<App />
</Router>
</Provider>,
document.getElementById("root")
);
You have a typo in your redux slice:
reducers: {
logout(state, action) {
// state.user = null; // <-- Typo error
state.loggedInUser = null; // This is the correct one
},
},
But, after fixing the typo, I would recommend that you refactor your code and create a "PrivateRoute" component as demoed in the docs:
const PrivateRoute = ({ children, ...rest }) => {
const { loggedInUser } = useSelector((state) => state.login);
return (
<Route
{...rest}
render={({ location }) =>
loggedInUser ? (
children
) : (
<Redirect to={{ pathname: "/login", state: { from: location } }} />
)
}
/>
);
};
and use it in App component for all the private routes:
<PrivateRoute path="/welcome">
<Welcome />
</PrivateRoute>
Now, this way, PrivateRoute will take care of "redirection" to the login page, all you need to do is "clear" the loggedInUser in your redux state.
Note that it is better to use "children" form when defining Routes. See What's the better way to configure routes in App.js in React with react-router-dom?
Try using:
import { useHistory } from "react-router-dom";
const history = useHistory();
const logoutSubmitHandler = () => {
history.push("/login");
}
I am migrating an app from Firebase to AWS Amplify. I want to create a React context which will provide route protection if the user is not logged in.
For example, my Auth.js file:
import React, { useEffect, useState, createContext } from 'react'
import fire from './firebase'
export const AuthContext = createContext()
export const AuthProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState(null)
useEffect(() => {
fire.auth().onAuthStateChanged(setCurrentUser)
}, [])
return (
<AuthContext.Provider value={{ currentUser }}>
{children}
</AuthContext.Provider>
)
}
And my App.js file:
import * as React from 'react'
import { BrowserRouter, Switch, Route } from 'react-router-dom'
import Navbar from './components/navbar/navbar'
import Home from './routes/Home'
import Register from './routes/Register'
import Footer from './components/footer/Footer'
import AlertProvider from './components/notification/NotificationProvider'
import MyAlert from './components/notification/Notification'
import { AuthProvider } from './Auth'
import PrivateRoute from './PrivateRoute'
const App = () => {
return (
<AuthProvider>
<BrowserRouter>
<AlertProvider>
<div className="app">
<Navbar />
<MyAlert />
<Switch>
<Route path="/" exact component={Home} />
<Route
path="/register"
exact
component={Register}
/>
<Route
path="/forgot-password"
render={(props) => <div>Forgot Password</div>}
/>
<Route path="*" exact={true} component={Home} />
</Switch>
<Footer />
</div>
</AlertProvider>
</BrowserRouter>
</AuthProvider>
)
}
export default App
This all works fine.
How would I do something similar with AWS Amplify? Essentially how would I create a Auth.js file that would wrap around my routes and give them a user context (which would update when the authentication status for the user is changed).
Thanks!
You can achieve this by setting up a custom protectedRoute HOC that will be used to protect any route that requires authentication. It will check if the user is signed-in and if the user is not signed-in then it will re-direct them to a specified route.
protectedRoute.js
import React, { useEffect } from 'react'
import { Auth } from 'aws-amplify'
const protectedRoute = (Comp, route = '/profile') => (props) => {
async function checkAuthState() {
try {
await Auth.currentAuthenticatedUser()
} catch (err) {
props.history.push(route)
}
}
useEffect(() => {
checkAuthState()
})
return <Comp {...props} />
}
export default protectedRoute
You can specify the default route or another route like the following:
// default redirect route
export default protectedRoute(Profile)
// custom redirect route
export default protectedRoute(Profile, '/sign-in')
You could also use the pre-built HOC from aws-amplify called withAuthenticator and that provides the UI as well as checking the users authentication status.
Sample use case for a profile page:
import React, { useState, useEffect } from 'react'
import { Button } from 'antd'
import { Auth } from 'aws-amplify'
import { withAuthenticator } from 'aws-amplify-react'
import Container from './Container'
function Profile() {
useEffect(() => {
checkUser()
}, [])
const [user, setUser] = useState({})
async function checkUser() {
try {
const data = await Auth.currentUserPoolUser()
const userInfo = { username: data.username, ...data.attributes, }
setUser(userInfo)
} catch (err) { console.log('error: ', err) }
}
function signOut() {
Auth.signOut()
.catch(err => console.log('error signing out: ', err))
}
return (
<Container>
<h1>Profile</h1>
<h2>Username: {user.username}</h2>
<h3>Email: {user.email}</h3>
<h4>Phone: {user.phone_number}</h4>
<Button onClick={signOut}>Sign Out</Button>
</Container>
);
}
export default withAuthenticator(Profile)
The routing for both would be the same and below I have linked a sample that I have used for both.:
import React, { useState, useEffect } from 'react'
import { HashRouter, Switch, Route } from 'react-router-dom'
import Nav from './Nav'
import Public from './Public'
import Profile from './Profile'
import Protected from './Protected'
const Router = () => {
const [current, setCurrent] = useState('home')
useEffect(() => {
setRoute()
window.addEventListener('hashchange', setRoute)
return () => window.removeEventListener('hashchange', setRoute)
}, [])
function setRoute() {
const location = window.location.href.split('/')
const pathname = location[location.length-1]
setCurrent(pathname ? pathname : 'home')
}
return (
<HashRouter>
<Nav current={current} />
<Switch>
<Route exact path="/" component={Public}/>
<Route exact path="/protected" component={Protected} />
<Route exact path="/profile" component={Profile}/>
<Route component={Public}/>
</Switch>
</HashRouter>
)
}
export default Router
I was working with the new auth0-spa and followed its tutorial to implement the auth0 SDK given by auth0 here.
I want to automatically redirect to the auth0 login page instead of a page that gives a button saying Log In.
Can anyone help me?
Here is my code
import PrivateRoute from "./PrivateRoute";
import React,{useState} from "react";
import { useAuth0 } from "../react-auth0-spa";
import {BrowserRouter, HashRouter, Link, Route, Router, Switch} from "react-router-dom";
import DefaultLayout from "../containers/DefaultLayout";
import history from "../utils/history";
const NavBar = () => {
const { isAuthenticated, loginWithRedirect, logout } = useAuth0();
return (
<div>
{!isAuthenticated && (
<button id="button" onClick={() => loginWithRedirect({})}>Log in</button>
)}
{isAuthenticated && (
<HashRouter history = {history}>
<Switch>
<PrivateRoute path="/" component={DefaultLayout}/>
</Switch>
</HashRouter>
)}
</div>
);
};
export default NavBar;
This is what I did:
const App: React.FC<{}> = () => {
const { isLoading, loginWithRedirect, user } = useAuth0();
useEffect(() => {
(async function login() {
if (!isLoading && !user) {
await loginWithRedirect();
}
})();
}, [isLoading]);
return (
<React.Fragment>
{/* your code */}
</React.Fragment>
)
}