How to: Authentication with RedwoodJS

Hi all :wave:

I tweeted out a guide I wrote about adding authentication to a RedwoodJS app (without using something like Netlify Identity or Magic Link) and the super friendly David S Price suggested I also post it here. What follows below is a summary of the more interesting hurdle I encountered; however you can read the article in its entirety at the following link (though you will probably want to skip more toward the end, past the Firebase and useContext set-up stuff): Adding Authentication to RedwoodJS (the hard way) - DEV Community.

It was actually more or less business as usual until it came time to protect sensitive routes, i.e. (in my case) conditionally render them based on if the user is authenticated or not. The first approach I thought of was using a higher-order component, say ProtectedRoute, which would look at the user auth context (if theyā€™re authenticated or not) and then either redirect to a login / unauthorized page or render the route appropriately. My understanding is that this is a relatively established pattern - in case you arenā€™t familiar with it though hereā€™s a pretty good guide: Protected Routes with React Function Components - DEV Community.

So once I had my ProtectedRoute component I replaced the Route components as needed in my Routes.js file, for example:

import ProtectedRoute from './ProtectedRoute'
[...]
    <Router>
      <Route path="/login" page={LoginPage} name="login" />
      <ProtectedRoute path="/orders" page={OrdersPage} name="orders" />
      <Route path="/" page={HomePage} name="home" />
    </Router>

The app reloaded successfully and I was happily thinking everything was good to go. However, when I attempted to access /orders unauthenticated, the app took me there without any problem! How could this be? I then did my classic go-to 10x developer debugging move and added a console.log('hiiii') to the ProtectedRoute component and to my surpriseā€¦ nothing was logged to the console :scream_cat:

So it seemed like for some reason the function wasnā€™t even being called. To be sure I changed the ProtectedRoute logic to simply return a string, something like ProtectedRoute = () => 'a' and sure enough the app continued to render as normal. Finally, taking a look at the source code, which admittedly I should have done from the beginning, I realized that what the components are within the parent Router component donā€™t really matter (as long as they contain the required props of course), as they are all read in and operated on directly as the children:

const routes = React.Children.toArray(children)
[...]
  for (let route of routes) {
    const { path, page: Page, redirect, notfound } = route.props
[...]
      const searchParams = parseSearch(search)
      const allParams = { ...pathParams, ...searchParams }
      if (redirect) {
        const newPath = replaceParams(redirect, pathParams)
        navigate(newPath)
        return (
          <RouterImpl pathname={newPath} search={search}>
            {children}
          </RouterImpl>
        )
      } else {
        return (
          <ParamsContext.Provider value={allParams}>
            <PageLoader
              spec={normalizePage(Page)}
              delay={pageLoadingDelay}
              params={allParams}
            />
          </ParamsContext.Provider>
        )
      }

So, admitting defeat, I first considered altering the Redwood source itself but then I noticed that another prop was being read in that I hadnā€™t seen in the router docs: redirect! This led me to setting a redirect prop on each sensitive Route in the default Routes.js, and then creating an AuthRoutes.js file with all the same routes, but without the redirect prop (see the guide for an example). The appropriate routes would then be loaded based on the userā€™s authentication status. That was it!

Iā€™m in no way claiming this is the best solution - considering it creates two sets of routes you have to maintain (& maybe another issue I havenā€™t thought of?) - but itā€™s been working pretty well for me so far. On that note - is there a reason the redirect prop isnā€™t documented (or maybe I missed it somewhere)? If people agree it would be useful I could write up a little section on it.

Another approach I tried, which I will just note briefly since this is already turning into a novel, was rendering only the public routes when the user was unauthenticated and then rendering all the routes once the user signed in. Something like so:

// Routes.js
      <Route path="/login" page={LoginPage} name="login" />
      <Route path="/" page={HomePage} name="home" />

// AuthRoutes.js
      <Route path="/login" page={LoginPage} name="login" />
      <Route path="/orders" page={OrdersPage} name="orders" />
      <Route path="/orders/new" page={NewOrderPage} name="newOrder" />
      <Route path="/" page={HomePage} name="home" />

However, upon successful login, every time I would attempt to access the ā€œ/ordersā€ page the app would break and I would get an error in the console about not being able to call the newOrder() function (called from the ā€œ/ordersā€ page). Iā€™m still not quite sure why this was happening :thinking: If anyone is curious I can add a more detailed reproduction example.

I hope some people find this interesting / useful. Any discussion is welcome!

8 Likes

This is awesome, thank you!

Weā€™re starting to create a formalised approach to authentication over here: https://github.com/redwoodjs/example-invoice/issues/31

It looks like Firebase is similar to our initial approach (based off your article) that we will probably be able to add support for it without much effort!

1 Like

@tmns Thanks for sharing! Iā€™m blocked on authentication, so this is much appreciated :slight_smile:

1 Like

My pleasure! Thank you and the rest of the team / contributors for taking the time and care to put together such a great framework! Iā€™ve really had a blast exploring it so far and itā€™s clear you all are really passionate about making something folks love to use :slightly_smiling_face:

Just out of curiosity - in looking a little at the invoice example app, I see in packages there is a hammer-auth-auth0 and its index.js imports from @hammerframework/web. Is this like a previous iteration of Redwood that also had a draft of authentication built out?

No problem - the possibility that someone else might also be facing a similar issue is the main reason I write stuff like this up :upside_down_face:

@tmns, thank you for the excellent article. Was very helpful in building a secured section for my app. I also built it with Firebase and in addition to the sign-in and sign-out I also added ā€œregisterā€ and ā€œforget passwordā€ actions. These works without any issue.
The problem arise, when I added ā€˜AuthRoutesā€™ and actually used them. the page is rendering, but I keep getting and warning message:

`Warning: Cannot update during an existing state transition (such as within `render`). Render methods should be a pure function of props and state.

in RouterImpl (created by LocationProvider)
in LocationProvider (created by Context.Consumer)
in Location (created by Router)
in Router (created by Routes)
in Routes (created by App)
in Suspense (created by App)
in App
in ProvideAuth
in ApolloProvider (created by GraphQLProvider)
in GraphQLProvider (created by RedwoodProvider)
in RedwoodProvider
in FatalErrorBoundary   `

and then:

Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.
    in PageLoader (created by RouterImpl)
    in RouterImpl (created by RouterImpl)

both are referencing, react-dom.development.js:88

I checked the Location.js, and it seems that it is created from the render() section here

any ideas?