Having the admin screens at
/admin is a reasonable thing to do. Let's update the routes to make that happen by updating the four routes where the URL begins with
/posts to start with
Head to http://localhost:8910/admin/posts and our generated scaffold page should come up. Thanks to named routes we don't have to update any of the
<Link>s that were generated by the scaffolds since the
names of the pages didn't change!
Having the admin at a different path is great, but nothing is stopping someone from just browsing to that new path and messing with our blog posts. How do we keep prying eyes away?
"Authentication" is a blanket term for all of the stuff that goes into making sure that a user, often identified with an email address and password, is allowed to access something. Authentication can be famously fickle to do right both from a technical and developer-happiness standpoint.
"Credentials" are the pieces of information a user provides to prove they are who they say they are: commonly a username (usually email) and password.
Redwood includes two authentication paths out of the box:
- Self-hosted, where user credentials are stored in your own database
- Third-party hosted, where user credentials are stored with the third party
In both cases you end up with an authenticated user that you can access in both the web and api sides of your app.
Redwood includes integrations for several of the most popular third-party auth providers:
- Netlify Identity
- Netlify GoTrue-JS
- Firebase's GoogleAuthProvider
As for our blog, we're going to use self-hosted authentication (named dbAuth in Redwood) since it's the simplest to get started with and doesn't involve any third party signups.
Authentication vs. Authorization
There are two terms which contain a lot of letters, starting with an "A" and ending in "ation" (which means you could rhyme them if you wanted to) that become involved in most discussions about login:
Here is how Redwood uses these terms:
- Authentication deals with determining whether someone is who they say they are, generally by "logging in" with an email and password, or a third party provider like Auth0.
- Authorization is whether a user (who has usually already been authenticated) is allowed to do something they want to do. This generally involves some combination of roles and permission checking before allowing access to a URL or feature of your site.
This section of the tutorial focuses on Authentication only. See part 2 of the tutorial to learn about Authorization in Redwood.
As you probably have guessed, Redwood has a couple of generators to get you going. One installs the backend components needed for dbAuth, the other creates login, signup and forgot password pages.
Run this setup command to get the internals of dbAuth added to our app:
When asked if you want to override the existing file
/api/src/lib/auth.js say yes. The shell
auth.js that's created in a new app make sure things like the
@requireAuth directive work, but now we'll replace it with a real implementation.
You'll see that the process creates several files and includes some post-install instructions for the last couple of customizations you'll need to make. Let's go through them now.
First we'll need to add a couple of fields to our
User model. We don't even have a
User model yet, so we'll create one along with the required fields at the same time.
schema.prisma and add:
This gives us a user with a name and email, as well as four fields that dbAuth will control:
- hashedPassword: stores the result of combining the user's password with a
saltand then hashed
- salt: a unique string that combines with the hashedPassword to prevent rainbow table attacks
- resetToken: if the user forgets their password, dbAuth inserts a token in here that must be present when the user returns to reset their password
- resetTokenExpiresAt: a timestamp after which the
resetTokenwill be considered expired and no longer valid (the user will need to fill out the forgot password form again)
Let's create the user model by migrating the database, naming it something like "create user":
That's it for the database setup!
Try reloading the Posts admin and we'll see something that's 50% correct:
Going to the admin section now prevents a non-logged in user from seeing posts, great! This is the result of the
@requireAuth directive in
api/src/graphql/posts.sdl: you're not authenticated so GraphQL will not respond to your request for data. But, ideally they wouldn't be able to see the admin pages themselves. Let's fix that with a new component in the Routes file,
We wrap the routes we want to be private (that is, only accessible when logged in) in the
<Private> component, and tell our app where to send them if they are unauthenticated. In this case they should go to the
Try going back to http://localhost:8910/admin/posts now and—yikes!
Well, we couldn't get to the admin pages, but we also can't see our blog posts any more. Do you know why we're seeing the same message here that we saw in the posts admin page?
It's because the
posts query in
posts.sdl is used by both the homepage and the posts admin page. Since it has the
@requireAuth directive, it's locked down and can only be accessed when logged in. But we do want people that aren't logged in to be able to view the posts on the homepage!
Now that our admin pages are behind a
<Private> route, what if we set the
posts query to be
@skipAuth instead? Let's try:
Reload the homepage and:
They're back! Let's just check that if we click on one of our posts that we can see it...UGH:
This page shows a single post, using the
post query, not
posts! So, we need to
@skipAuth on that one as well:
Cross your fingers and reload!
We're back in business! Once you add authentication into your app you'll probably run into several situations like this where you need to go back and forth, re-allowing access to some pages or queries that inadvertently got locked down by default. Remember, Redwood is secure by default—we'd rather you accidentally expose too little of your app than too much!
Now that our pages are behind login, let's actually create a login page so that we can see them again.
Skipping auth altogether for
postfeels bad somehow...
Ahh, good eye. While posts don't currently expose any particularly secret information, what if we eventually add a field like
publishStatuswhere you could mark a post as
draftso that it doesn't show on the homepage. But, if you knew enough about GraphQL, you could easily request all posts in the database and be able to read all the drafts!
It would be more future-proof to create a new endpoint for public display of posts, something like
publicPostthat will have built-in logic to only ever return a minimal amount of data and leave the default
postqueries returning all the data for a post, something that only the admin will have access to. (Or do the opposite: keep
postas public and create new
adminPostendpoints that can contain sensitive information.)
Yet another generator is here for you, this time one that will create pages for login, signup and forgot password pages:
Again several pages will be created and some post-install instructions will describe next steps. But for now, try going to http://localhost:8910/login:
That was easy! We don't have a user to login with, so try going to the signup page instead (there's a link under the Login button, or just head to http://localhost:8910/signup):
dbAuth defaults to the generic "Username" for the first field, but in our case the username will be an email address (we can change that label in a moment). Create yourself a user with email and password:
And after clicking "Signup" you should end up back on the homepage, where everything looks the same! Yay? But now try going to http://localhost:8910/admin/posts:
Awesome! Signing up will automatically log you in (although this behavior can be changed) and if you look in the code for the
SignupPage you'll see where the redirect to the homepage takes place (hint: check out line 21).
Now that we're logged in, how do we log out? Let's add a link to the
BlogLayout so that it's present on all pages, and also include an indicator of who you're actually logged in as.
Redwood provides a hook
useAuth which we can use in our components to determine the state of the user's login-ness, get their user info, and more. In
BlogLayout we want to destructure the
logOut properties from
As you can probably tell by the names:
- isAuthenticated: a boolean as to whether or not a user is logged in
- currentUser: any details the app has on that user (more on this in a moment)
- logOut: removes the user's session and logs them out
At the top right of the page, let's show the email address of the user (if they're logged in) as well as a link to log out. If they're not logged in, let's show a link to do just that:
Well, it's almost right! Where's our email address? By default, the function that determines what's in
currentUser only returns that user's
id field for security reasons (better to expose too little than too much, remember!). To add email to that list, check out
getCurrentUser() function is where the magic happens: whatever is returned by this function is the content of
currentUser, in both the web and api sides! In the case of dbAuth, the single argument passed in,
session, contains the
id of the user that's logged in. It then looks up the user in the database with Prisma, selecting just the
id. Let's add
Now our email should be present at the upper right on the homepage:
Before we leave this file, take a look at
requireAuth(). Remember when we talked about the
@requireAuth directive and how when we first installed authentication we saw the message "You don't have permission to do that."? This is where that came from!
Believe it or not, that's pretty much it for authentication! You can use the combination of
@skipAuth directives to lock down access to GraphQL query/mutations, and the
<Private> component to restrict access to entire pages of your app. If you only want to restrict access to certain components, or certain parts of a component, you can always get
isAuthenticated from the
useAuth() hook and then render one thing or another.
Remember the GraphQL Playground exercise at the end of Creating a Contact? Try to run that again now that authentication is in place and you should get that error we've been talking about because of the
@requireAuth directive! But, creating a new contact should still work just fine (because we're using
@skipAuth on that mutation).
However, simulating a logged-in user through the GraphQL Playground is no picnic. But, we're working on improving the experience!