Hi all
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
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 If anyone is curious I can add a more detailed reproduction example.
I hope some people find this interesting / useful. Any discussion is welcome!