a first look at redwoodJS part 7 - authentication, netlify identity
redwoodjsreactnetlifyjamstackUnfortunately I do not have any epic Tom quotes about authentication, so I'll leave you with this:
I have plenty of ideas.
Tom Preston-Werner - RedwoodJS with Tom Preston-Werner
Part 7 - Authentication #
In this penultimate part we'll add authentication to our application, something that has never confused any developer ever. We're going to implement a login form so no one can edit our blog willy-nilly. We will accomplish this with:
- @redwoodjs/auth - lightweight wrapper around popular SPA auth libraries
- Netlify Identity Widget - component for authenticating with Netlify's Identity service
However, this is not the only way to implement authentication with Redwood. Redwood currently supports a wide range of authentication providers including:
- Auth0
- Azure Active Directory
- Netlify GoTrue-JS
- Magic Links - Magic.js
- Ethereum
- Supabase
- Nhost
- Firebase's GoogleAuthProvider
Check out the Auth Playground to explore the other providers.
7.1 Administration #
The first step we will take on our journey to securing a Redwood applications includes updating the /posts routes to include admin screens at /admin. The four routes starting with /posts will now start with /admin/posts instead:
// web/src/Routes.js
<Route
path="/admin/posts/new"
page={NewPostPage}
name="newPost"
/>
<Route
path="/admin/posts/{id:Int}/edit"
page={EditPostPage}
name="editPost"
/>
<Route
path="/admin/posts/{id:Int}"
page={PostPage}
name="post"
/>
<Route
path="/admin/posts"
page={PostsPage}
name="posts"
/>
Open http://localhost:8910/admin/posts to see the generated scaffold page. We don't have to update any of the <Link>s that were generated by the scaffolds because the named route functions didn't change. We have now moved the route to a different path, but it is still an unsecured route.
7.2 rw generate auth #
A subset of the previously mentioned auth providers can also be easily configured by using the rw generate auth command.
yarn rw setup auth <provider>
Choose the provider option based on your own authentication provider. Supported options include:
auth0firebasegoTruemagicLinknetlify
Since we are using the Netlify Identity Widget we will use the netlify option.
yarn rw setup auth netlify
This will create an auth.js file in api/src/lib and automatically modify our index.js file in web/src and our graphql.js file in api/src/functions.
✔ Generating auth lib...
✔ Successfully wrote file `./api/src/lib/auth.js`
✔ Adding auth config to web...
✔ Adding auth config to GraphQL API...
✔ Adding required web packages...
✔ Installing packages...
✔ One more thing...
You will need to enable Identity on your Netlify site and configure the API endpoint.
See: https://github.com/netlify/netlify-identity-widget#localhost
If we check our package.json file in our web folder we'll see two new dependencies:
@redwoodjs/authnetlify-identity-widget
AuthProvider #
If we check our App.js file in web/src we will see the code modifications that were performed by the yarn rw setup auth command.
// web/src/App.js
import { AuthProvider } from '@redwoodjs/auth'
import netlifyIdentity from 'netlify-identity-widget'
import { isBrowser } from '@redwoodjs/prerender/browserUtils'
isBrowser && netlifyIdentity.init()
const App = () => (
<FatalErrorBoundary page={FatalErrorPage}>
<AuthProvider
client={netlifyIdentity}
type="netlify"
>
<RedwoodApolloProvider>
<Routes />
</RedwoodApolloProvider>
</AuthProvider>
</FatalErrorBoundary>
)
export default App
AuthProvider wraps the Router and takes in a client and type which are netlifyIdentity and netlify respectively.
getCurrentUser #
Our graphql.js file in the api/src/functions directory is importing getCurrentUser and then passing it to the handler that sets up our GraphQL API so we don't have to worry about it.
// api/src/functions/graphql.js
import { getCurrentUser } from 'src/lib/auth'
export const handler = createGraphQLHandler({
getCurrentUser,
schema: makeMergedSchema({
schemas,
services: makeServices({ services }),
}),
})
auth.js #
The auth.js file was generated in our api/src folder.

Here is the generated file with comments removed.
// api/src/lib/auth.js
import {
AuthenticationError,
ForbiddenError,
parseJWT
} from '@redwoodjs/api'
export const getCurrentUser = async (decoded, { _token, _type }, { _event, _context }) => {
return { ...decoded, roles: parseJWT({ decoded }).roles }
}
export const requireAuth = ({ role } = {}) => {
if (!context.currentUser) {
throw new AuthenticationError("You don't have permission to do that.")
}
if (
typeof role !== 'undefined' &&
typeof role === 'string' &&
!context.currentUser.roles?.includes(role)
) {
throw new ForbiddenError("You don't have access to do that.")
}
if (
typeof role !== 'undefined' &&
Array.isArray(role) &&
!context.currentUser.roles?.some((r) => role.includes(r))
) {
throw new ForbiddenError("You don't have access to do that.")
}
}
7.3 API Authentication #
First let's lock down the API so we can be sure that only authorized users can create, update and delete a Post.
requireAuth #
We'll import requireAuth and use it to restrict access to our endpoints. It is a helper method used in our services. If someone is not authenticated it will throw an error.
// api/src/services/posts/posts.js
import { db } from 'src/lib/db'
import { requireAuth } from 'src/lib/auth'
export const posts = () => {
return db.post.findMany()
}
export const post = ({ id }) => {
return db.post.findUnique({
where: { id },
})
}
export const createPost = ({ input }) => {
requireAuth()
return db.post.create({
date: input,
})
}
export const updatePost = ({ id, input }) => {
requireAuth()
return db.post.update({
data: input,
where: { id },
})
}
export const deletePost = ({ id }) => {
requireAuth()
return db.post.delete({
where: { id },
})
}
We want to restrict access to the sensitive endpoints including createPost, updatePost, and deletePost.

Try to make a new post.

7.4 Web Authentication #
Now we'll restrict access to the admin pages completely unless you're logged in. The first step will be to denote which routes will require that you be logged in.
Private #
We were told that we don't have permission to create a post. But we want to make it so an unauthenticated user can't even get this far. Let's create private routes for all the /posts routes. We'll import Private from @redwoodjs/router and use it to wrap our /posts routes.
// web/src/Routes.js
import { Router, Route, Set, Private } from '@redwoodjs/router'
import BlogPostLayout from 'src/layouts/BlogPostLayout'
const Routes = () => {
return (
<Router>
<Set wrap={BlogPostLayout}>
<Route path="/blog-post/{id:Int}" page={BlogPostPage} name="blogPost" />
<Route path="/contact" page={ContactPage} name="contact" />
<Route path="/about" page={AboutPage} name="about" />
<Route path="/" page={HomePage} name="home" />
</Set>
<Private>
<Route path="/admin/posts/new" page={NewPostPage} name="newPost" />
<Route path="/admin/posts/{id:Int}/edit" page={EditPostPage} name="editPost" />
<Route path="/admin/posts/{id:Int}" page={PostPage} name="post" />
<Route path="/admin/posts" page={PostsPage} name="posts" />
</Private>
<Route notfound page={NotFoundPage} />
</Router>
)
}
export default Routes
If we go back to the new route we'll now get an error message.

Instead of just showing an error message, we can redirect the user back to the home page by adding unauthenticated="home".
<Private unauthenticated="home">
<Route
path="/posts/new"
page={NewPostPage}
name="newPost"
/>
<Route
path="/posts/{id:Int}/edit"
page={EditPostPage}
name="editPost"
/>
<Route
path="/posts/{id:Int}"
page={PostPage}
name="post"
/>
<Route
path="/posts"
page={PostsPage}
name="posts"
/>
</Private>
Now we'll see a redirect in the URL and we'll be taken back to the home

useAuth #
To implement login we'll import useAuth from the Redwood auth package and pull out the logIn object with object destructuring. We'll then add a link to Login and pass the logIn object to the onClick event handler.
import { Link, routes } from '@redwoodjs/router'
import { useAuth } from '@redwoodjs/auth'
const BlogLayout = ({ children }) => {
const { logIn } = useAuth()
return (
<>
<header>
<h1>
<Link to={routes.home()}>ajcwebdev</Link>
</h1>
<nav>
<ul>
<li>
<Link to={routes.about()}>
About
</Link>
</li>
<li>
<Link to={routes.contact()}>
Contact
</Link>
</li>
<li>
<a href="#" onClick={logIn}>
Login
</a>
</li>
</ul>
</nav>
</header>
<main>{children}</main>
</>
)
}
export default BlogLayout
If we return to our browser we'll now see a link to log in.

7.5 Netlify Identity #
@redwoodjs/auth is a lightweight wrapper around popular SPA authentication libraries. I'm going to use the Netlify Identity Widget, however Redwood can support a wide range of authentication providers including:
- Auth0
- Azure Active Directory
- Netlify GoTrue-JS
- Magic Links - Magic.js
- Ethereum
- Supabase
- Nhost
- Firebase's GoogleAuthProvider
Check out the Auth Playground for working examples of different authentication providers.
Go to the Identity tab #

Click Enable Identity to enable identity #

Here we can invite users to give them permissions. We are going to lock down our site and only give ourselves permission to see or edit anything.
Click Invite user to invite a user #

If we click the link we'll get this fancy looking message from Netlify asking for our site's url.

Enter the domain that we created at the beginning of the article.

Now we have our log in forum.

You should have received an email to accept the invitation from Netlify.

If we follow the link to accept the invite we'll be taken to our live website and there will be an invite token in the URL.

Grab the invite_token starting with the # and copy-paste it over to your localhost.

You should now get a form to enter your password and complete your signup.

If you did everything correctly then you should see your blog posts again.

Now we want to add a link to our home page that we can use to log in and log out. We'll destructure two addition objects, isAuthenticated and logOut.
const BlogLayout = ({ children }) => {
const {
logIn,
isAuthenticated,
logOut
} = useAuth()
We'll add another list them that uses a ternary operator to check whether we are authenticated and to display either Log Out or Log In depending on whether we are logged in or not.
<li>
<a href="#" onClick={logIn} >
{ isAuthenticated ? 'Log Out' : 'Log In'}
</a>
</li>
Go back to your browser and since we are logged in you should see a link for Log Out.

Now lets also add a ternary operator to the link itself so it knows to log out with we are currently logged in, and log in if we aren't currently logged in.
<li>
<a
href="#"
onClick={isAuthenticated ? logOut : logIn}
>
{ isAuthenticated ? 'Log Out' : 'Log In'}
</a>
</li>
If we click Log Out the page will refresh and the link will change to Log In.

If we click Log In then we see our log in form again.

If we log in then the link will change back to Log Out.

We'll destructure one more object called currentUser.
const BlogLayout = ({ children }) => {
const {
logIn,
isAuthenticated,
logOut,
currentUser
} = useAuth()
We'll check to make sure we're authenticated and if so we'll show the current user's email with currentUser.email.
{isAuthenticated && <li>Logged in as {currentUser.email}</li>}
If we now look back in our browser you should see a message saying you are logged in.

In the next part we'll finally deploy our project to the internet with the universal deployment machi.... I mean, Netlify.