DEV Community

Cover image for a first look at redwoodJS part 7 - authentication, netlify identity
anthony-campolo
anthony-campolo

Posted on • Updated on

a first look at redwoodJS part 7 - authentication, netlify identity

Unfortunately 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:

However, this is not the only way to implement authentication with Redwood. Redwood currently supports a wide range of authentication providers including:

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"
/>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 }),
  }),
})
Enter fullscreen mode Exit fullscreen mode

auth.js

The auth.js file was generated in our api/src folder.

11-lib-auth.js

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.")
  }
}
Enter fullscreen mode Exit fullscreen mode

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 },
  })
}
Enter fullscreen mode Exit fullscreen mode

We want to restrict access to the sensitive endpoints including createPost, updatePost, and deletePost.

14-test-new-post

Try to make a new post.

15-don't-have-permission-to-make-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
Enter fullscreen mode Exit fullscreen mode

If we go back to the new route we'll now get an error message.

16-something-went-wrong

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>
Enter fullscreen mode Exit fullscreen mode

Now we'll see a redirect in the URL and we'll be taken back to the home

17-redirect-to-home-page

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
Enter fullscreen mode Exit fullscreen mode

If we return to our browser we'll now see a link to log in.

18-home-page-with-log-in-link

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:

Check out the Auth Playground for working examples of different authentication providers.

Go to the Identity tab

06-netlify-identity

Click Enable Identity to enable identity

07-netlify-identity-level-0

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

08-invite-users

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

19-url-of-your-netlify-site

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

20-set-site's-url

Now we have our log in forum.

21-log-in-form

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

22-you've-been-invited-to-join

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.

23-deployed-site-invite-token

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

24-localhost-invite-token

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

25-complete-your-signup

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

26-protected-posts

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()
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Go back to your browser and since we are logged in you should see a link for Log Out.

27-home-page-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>
Enter fullscreen mode Exit fullscreen mode

If we click Log Out the page will refresh and the link will change to Log In.

28-home-page-log-in-link

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

29-log-in-form

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

30-home-page-log-out-link

We'll destructure one more object called currentUser.

const BlogLayout = ({ children }) => {
  const {
    logIn,
    isAuthenticated,
    logOut,
    currentUser
  } = useAuth()
Enter fullscreen mode Exit fullscreen mode

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>}
Enter fullscreen mode Exit fullscreen mode

If we now look back in our browser you should see a message saying you are logged in.

31-logged-in-as

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

Discussion (0)