Search code examples
reactjsamazon-web-servicesaws-amplifyreact-context

Protected Routes with AWS Amplify using React context


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!


Solution

  • 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