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:
auth0
firebase
goTrue
magicLink
netlify
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/auth
netlify-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.