I'm trying to build a web application using AWS Amplify. I have configured authentication but I want certain pages to be only available to authenticated users, for example the home page can be seen by anyone but "/dashboard" can only be seen by logged in users. I'm currently using AWS Amplify for my backend and a React frontend, using react-router v6 to route between pages.
Currently, my routing code is very simple (this is my first time using React) and is in App.js:
import React from 'react'; import { BrowserRouter, Route, Routes, } from 'react-router-dom'; import Login from './pages/Login'; import Home from './pages/Home'; import Dashboard from './pages/Dashboard'; import ErrorPage from './pages/ErrorPage'; const App = () => { return ( <BrowserRouter> <Routes> <Route exact path="/" element={<Home />} /> <Route path="/login" element={<Login />} /> <Route path="/dashboard" element={<Dashboard />} /> <Route path="*" element={<ErrorPage />} /> </Routes> </BrowserRouter> ); } export default App;
I first tried using withAuthenticator
to wrap the page I wanted to authenticate, but this just resulted in a loop showing the login box.
function Dashboard({ signOut, user }) { return ( <> <h1>Hello {user.username}, this is still in development.</h1> <button onClick={signOut}> Sign out</button> </> ); } export default withAuthenticator(Dashboard);
I also tried adding a function to check if the user is authenticated and return something different, but this just shows a blank screen for authenticated and unauthenticated users. I assume this is because it's async
, but I'm not familiar enough with React to understand why and how to fix it.
async function isAuthed() { try { await Auth.currentAuthenticatedUser(); return true; } catch(e) { return false; } } async function Dashboard() { if (await isAuthed()) { return ( <> <h1>Hello, this is still in development.</h1> </> ); } else { return ( <> <h1>Please login to view this page.</h1> </> ) } }
I also tried to see if there was some way to route asynchronously, but not sure how to implement it.
edit:
@Jlove's solution already works as expected, my updated App.js
routing code is as follows:
import React, { useState, useEffect } from 'react'; import { BrowserRouter, Route, Routes, useNavigate, } from 'react-router-dom'; import { Amplify, Auth } from 'aws-amplify' import Login from './pages/Login'; import Home from './pages/Home'; import Dashboard from './pages/Dashboard'; import ErrorPage from './pages/ErrorPage'; import Unauthenticated from './pages/Unauthenticated'; function RequireAuth({ children }) { const navigate = useNavigate(); const [isAuth, setIsAuth] = useState(null); useEffect(() => { Auth.currentAuthenticatedUser() .then(() => setIsAuth(true)) .catch(() => { navigate("/unauthenticated") }) }, []) return isAuth && children; } const App = () => { return ( <BrowserRouter> <Routes> <Route exact path="/" element={<Home />} /> <Route path="/login" element={<Login />} /> <Route path="/dashboard" element={ <RequireAuth> <Dashboard /> </RequireAuth> } /> <Route path="*" element={<ErrorPage />} /> <Route path="/unauthenticated" element={<Unauthenticated />} /> </Routes> </BrowserRouter> ); } export default App;
P粉7258276862023-09-10 10:13:10
You will want to separate the logic that protects your routes from what each route renders. Don't mix authentication with the UI/content components you want to render on the route.
A common protection pattern is to use layout routing to wrap the entire route group to which you want to protect access. You will create a layout route component that triggers an effect to check the authentication status of the current user and conditionally return:
Outlet
for protected content (if the user has been authenticated)This prevents (a) accidental access to a protected page before it is known that the user is not authenticated, and (b) accidental redirection to the login page before it is known that the user has been authenticated.
Example:
const checkAuthentication = async () => {
try {
await Auth.currentAuthenticatedUser();
return true;
} catch {
return false;
}
};
import { Outlet, Navigate } from 'react-router-dom'; const ProtectedRoute = () => { const [isAuth, setIsAuth] = useState(undefined); useEffect(() => { checkAuthentication() .then(() => setIsAuth(true)) .catch(() => setIsAuth(false)); }, []); if (isAuth === undefined) { return null; // 或加载中的旋转器/指示器等 } return isAuth ? <Outlet /> : <Navigate to="/login" replace />; }
Wrapping requires protected routing.
import React from 'react'; import { BrowserRouter, Route, Routes, } from 'react-router-dom'; import Login from './pages/Login'; import Home from './pages/Home'; import Dashboard from './pages/Dashboard'; import ErrorPage from './pages/ErrorPage'; import ProtectedRoute from './components/ProtectedRoute'; const App = () => { return ( <BrowserRouter> <Routes> <Route path="/" element={<Home />} /> <Route path="/login" element={<Login />} /> <Route element={<ProtectedRoute />}> <Route path="/dashboard" element={<Dashboard />} /> {/* ... 其他受保护的路由 ... */} </Route> <Route path="*" element={<ErrorPage />} /> </Routes> </BrowserRouter> ); } export default App;
P粉9785510812023-09-10 09:28:28
Here is one way to do it by wrapping component routing in an authorization component:
<Route path="/somePathToProtect" element={ <RequireAuth> <Dashboard /> </RequireAuth> } /> export function RequireAuth({children}) { const navigate = useNavigate(); const [isAuth, setIsAuth] = useState(null); useEffect(() => { Auth.currentAuthenticatedUser() .then( () => setIsAuth(true) ) .catch(() => { navigate('/routeToCatchNonAuth') }) }, []) return isAuth && children; }
The goal here is to secure your routes based on the results returned by Auth
. If Auth
selects the catch route, use the router to navigate the user to an unauthorized page.