3

I am trying to build a web app with AWS Amplify. I have authentication configured, but I want certain pages to be for authenticated users only e.g. anyone can see the home page, but only logged in users should see "/dashboard". I am currently using AWS Amplify as my backend and a React front-end, using react-router v6 to route between pages.

Currently, my routing code is very naive (it 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 wrapping the page I want gated with withAuthenticator, but this just caused a loop of seeing 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 have also tried to add a function to check if the user is authenticated, and return different bodies, but this just shows a white screen for both an authenticated and non-authenticated users. I believe it is because it is async, but I am not familiar enough with react to understand why or 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 have also tried to see if there is some method of asynchronous routing, but not sure how I would implement that.

Edit:

@Jlove's solution has worked as intended, my updated App.js routing code is now:

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;
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
supinie
  • 53
  • 5
  • For general route protection my answer [here](https://stackoverflow.com/a/66289280/8690857) may be helpful, but can you [edit] the post to include relevant route protection code you are using? See [mcve]. – Drew Reese Jul 20 '23 at 20:59
  • Your comment is the first I have actually read about route protection in use - I think that my naivity and inexperience didn't help me trying to find the correct search terms. I will update my original question with both my original routing code, as well as the updated version based on the solution below by @Jlove that has worked for me. – supinie Jul 20 '23 at 22:19
  • When `Auth.currentAuthenticatedUser()` resolves, does that ***always*** mean the user is successfully authenticated, or can it resolve and basically say the user check failed and the current user is not authenticated? – Drew Reese Jul 20 '23 at 23:09
  • That is correct, `Auth.currentAuthenticatedUser()` will return a user object for the user currently authenticated, if there is no currently authenticated user, then it will return an error ([source](https://docs.amplify.aws/lib/auth/manageusers/q/platform/js/#retrieve-current-authenticated-user)). – supinie Jul 21 '23 at 00:03

2 Answers2

1

Here's one way to go about this by wrapping your component route 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 gatekeep your route based on what Auth returns. If Auth takes the catch route, utilize the router to navigate the user to wherever you want unauthorized users to go.

Jlove
  • 239
  • 1
  • 7
1

You will want to keep separate the logic of protecting routes from the content each route renders. Don't mix in authentication with the UI/content components you want to render on routes.

A common protection pattern is to use a layout route to wrap an entire group of routes you want to protect access to. You will create a layout route component that triggers an effect to check the current user's authentication status, and conditionally return:

  • Nothing/loading (if status isn't known yet)
  • An Outlet for the protected content (if user is authenticated),
  • Aa redirect to a non-protected route (if user is unauthenticated).

The prevents (a) accidental access to protected pages prior to knowing a user is unauthenticated and (b) accidental redirects to login prior to knowing a user was really already 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; // or loading spinner/indicator/etc
  }
    
  return isAuth ? <Outlet /> : <Navigate to="/login" replace />;
}

Wrap the routes that need to be auth-protected.

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 />} />
          {/* ... other protected routes ... */}
        </Route>
        <Route path="*" element={<ErrorPage />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • Thanks for your response and explanation Drew, that makes a lot of sense, and I really like the ease of extensibility. When `ProtectedRoute` is evaluating `isAuth`, if it is undefined, when it resolves, I'm assuming it will re-route to the corresponding path, but I'm not too sure how/why. – supinie Jul 21 '23 at 08:19
  • @supinie It's *technically* already on the route, but the parent layout route is applying some conditional rendering for the current condition. When the `isAuth` resolves and the user is authenticated, the `Outlet` is rendered which allows the *already matched* route to then actually output its content to the DOM. – Drew Reese Jul 21 '23 at 08:21
  • Ahhh, okay that makes lots of sense to think about it that way, thanks! – supinie Jul 21 '23 at 08:22