Skip to main content

Making a site with user authentication

Sometimes, you need to create a site with gated content, available only to authenticated users. Using Gatsby, you may achieve this using the concept of client-only routes, to define which pages a user can view only after logging in.

Prerequisites

You should have already configured your environment to be able to use the gatsby-cli. A good starting point is the main tutorial.

Security notice

In production, you should use a tested and robust solution to handle the authentication. Auth0, Firebase, and Passport.js are good examples. This tutorial will only cover the authentication workflow, but you should take the security of your app as seriously as possible.

Building your Gatsby app

Start by creating a new Gatsby project:

gatsby new gatsby-auth
cd gatsby-auth

Then, add a more apt title to your newly created site, changing the content of gatsby-config.js:

gatsby-config.js
module.exports = {
  siteMetadata: {
    title: "Gatsby Authentication Tutorial",
  },
  plugins: ["gatsby-plugin-react-helmet", "gatsby-plugin-offline"],
}

Create a new component to hold the links. For now, it will act as a placeholder:

src/components/navBar.js
import React from "react"
import { Link } from "gatsby"

export default () => (
  <div
    style={{
      display: "flex",
      flex: "1",
      justifyContent: "space-between",
      borderBottom: "1px solid #d1c1e0",
    }}
  >
    <span>You are not logged in</span>

    <nav>
      <Link to="/">Home</Link>
      {` `}
      <Link to="/">Profile</Link>
      {` `}
      <Link to="/">Logout</Link>
    </nav>
  </div>
)

And edit the layout component to include it:

src/components/layout.js
import React from "react"
import PropTypes from "prop-types"
import Helmet from "react-helmet"
import { StaticQuery, graphql } from "gatsby"

import Header from "./header"
import NavBar from "./navBar"import "./layout.css"

const Layout = ({ children }) => (
  <StaticQuery
    query={graphql`
      query SiteTitleQuery {
        site {
          siteMetadata {
            title
          }
        }
      }
    `}
    render={data => (
      <>
        <Helmet
          title={data.site.siteMetadata.title}
          meta={[
            { name: "description", content: "Sample" },
            { name: "keywords", content: "sample, something" },
          ]}
        >
          <html lang="en" />
        </Helmet>
        <Header siteTitle={data.site.siteMetadata.title} />
        <div
          style={{
            margin: "0 auto",
            maxWidth: 960,
            padding: "0px 1.0875rem 1.45rem",
            paddingTop: 0,
          }}
        >
          <NavBar />           {children}
        </div>
      </>
    )}
  />
)

Layout.propTypes = {
  children: PropTypes.node.isRequired,
}

export default Layout

Lastly, change the index page to include this new content:

src/pages/index.js
import React from "react"
import { Link } from "gatsby"

import Layout from "../components/layout"

const IndexPage = () => (
  <Layout>
    <h1>Hi people</h1>
    <p>      You should <Link to="/">log in</Link> to see restricted content    </p>  </Layout>
)

export default IndexPage

Authentication service

For this tutorial you will use a hardcoded user/password. Create the folder src/services and add the following content to the file auth.js:

src/services/auth.js
export const isBrowser = () => typeof window !== "undefined"

export const getUser = () =>
  isBrowser() && window.localStorage.getItem("gatsbyUser")
    ? JSON.parse(window.localStorage.getItem("gatsbyUser"))
    : {}

const setUser = user =>
  window.localStorage.setItem("gatsbyUser", JSON.stringify(user))

export const handleLogin = ({ username, password }) => {
  if (username === `john` && password === `pass`) {
    return setUser({
      username: `john`,
      name: `Johnny`,
      email: `johnny@example.org`,
    })
  }

  return false
}

export const isLoggedIn = () => {
  const user = getUser()

  return !!user.username
}

export const logout = callback => {
  setUser({})
  callback()
}

Creating client-only routes

At the beginning of this tutorial, you created a “default” Gatsby site, which includes the @reach/router library. Now, using the @reach/router library, you can create routes available only to logged-in users. This library is used by Gatsby under the hood, so you don’t even have to install it.

First, edit gatsby-node.js. You will define that any route that starts with /app/ is part of your restricted content and the page will be created on demand:

gatsby-node.js
// Implement the Gatsby API “onCreatePage”. This is
// called after every page is created.
exports.onCreatePage = async ({ page, actions }) => {
  const { createPage } = actions

  // page.matchPath is a special key that's used for matching pages
  // only on the client.
  if (page.path.match(/^\/app/)) {
    page.matchPath = "/app/*"

    // Update the page.
    createPage(page)
  }
}

Note: There is a convenient plugin that already does this work for you: gatsby-plugin-create-client-paths

Now, you must create a generic page that will have the task to generate the restricted content:

src/pages/app.js
import React from "react"
import { Router } from "@reach/router"
import Layout from "../components/layout"
import Profile from "../components/profile"
import Login from "../components/login"

const App = () => (
  <Layout>
    <Router>
      <Profile path="/app/profile" />
      <Login path="/app/login" />
    </Router>
  </Layout>
)

export default App

Next, add the components regarding those new routes. The profile component to show the user data:

src/components/profile.js
import React from "react"

const Profile = () => (
  <>
    <h1>Your profile</h1>
    <ul>
      <li>Name: Your name will appear here</li>
      <li>E-mail: And here goes the mail</li>
    </ul>
  </>
)

export default Profile

The login component will handle - as you may have guessed - the login process:

src/components/login.js
import React from "react"
import { navigate } from "gatsby"
import { handleLogin, isLoggedIn } from "../services/auth"

class Login extends React.Component {
  state = {
    username: ``,
    password: ``,
  }

  handleUpdate = event => {
    this.setState({
      [event.target.name]: event.target.value,
    })
  }

  handleSubmit = event => {
    event.preventDefault()
    handleLogin(this.state)
  }

  render() {
    if (isLoggedIn()) {
      navigate(`/app/profile`)
    }

    return (
      <>
        <h1>Log in</h1>
        <form
          method="post"
          onSubmit={event => {
            this.handleSubmit(event)
            navigate(`/app/profile`)
          }}
        >
          <label>
            Username
            <input type="text" name="username" onChange={this.handleUpdate} />
          </label>
          <label>
            Password
            <input
              type="password"
              name="password"
              onChange={this.handleUpdate}
            />
          </label>
          <input type="submit" value="Log In" />
        </form>
      </>
    )
  }
}

export default Login

Though the routing is working now, you still can access all routes without restriction.

Controlling private routes

To check if a user can access the content, you can wrap the restricted content inside a PrivateRoute component:

scr/components/privateRoute.js
import React from "react"
import { navigate } from "gatsby"
import { isLoggedIn } from "../services/auth"

const PrivateRoute = ({ component: Component, location, ...rest }) => {
  if (!isLoggedIn() && location.pathname !== `/app/login`) {
    // If the user is not logged in, redirect to the login page.
    navigate(`/app/login`)
    return null
  }

  return <Component {...rest} />
}

export default PrivateRoute

And now you can edit your Router to use the PrivateRoute component:

src/pages/app.js
import React from "react"
import { Router } from "@reach/router"
import Layout from "../components/layout"
import PrivateRoute from "../components/privateRoute"import Profile from "../components/profile"
import Login from "../components/login"

const App = () => (
  <Layout>
    <Router>
      <PrivateRoute path="/app/profile" component={Profile} /> <Login path="/app/login" />    </Router>
  </Layout>
)

export default App

Refactoring to use new routes and user data

With the client-only routes in place, you must now refactor some files to account for the user data available.

The navigation bar will show the user name and logout option to registered users:

src/components/navBar.js
import React from "react"
import { Link, navigate } from "gatsby"import { getUser, isLoggedIn, logout } from "../services/auth"
export default () => {  const content = { message: "", login: true }  if (isLoggedIn()) {    content.message = `Hello, ${getUser().name}`  } else {    content.message = "You are not logged in"  }  return (    <div
      style={{
        display: "flex",
        flex: "1",
        justifyContent: "space-between",
        borderBottom: "1px solid #d1c1e0",
      }}
    >
      <span>{content.message}</span> 
      <nav>
        <Link to="/">Home</Link>
        {` `}
        <Link to="/app/profile">Profile</Link>         {` `}
        {isLoggedIn() ? (          <a            href="/"            onClick={event => {              event.preventDefault()              logout(() => navigate(`/app/login`))            }}          >            Logout          </a>        ) : null}      </nav>
    </div>
  )
}

The index page will suggest to login or check the profile accordingly:

src/pages/index.js
import React from "react"
import { Link } from "gatsby"
import { getUser, isLoggedIn } from "../services/auth"
import Layout from "../components/layout"

const IndexPage = () => {  return (    <Layout>
      <h1>Hi {isLoggedIn() ? getUser().name : "people"}</h1>      <p>        {isLoggedIn() ? (          <>            You are logged in, so check your{" "}            <Link to="/app/profile">profile</Link>          </>        ) : (          <>            You should <Link to="/app/login">log in</Link> to see restricted            content          </>        )}      </p>    </Layout>
  )
}
export default IndexPage

And the profile will show the user data:

src/components/profile.js
import React from "react"
import { getUser } from "../services/auth"
const Profile = () => (
  <>
    <h1>Your profile</h1>
    <ul>
      <li>Name: {getUser().name}</li>      <li>E-mail: {getUser().email}</li>    </ul>
  </>
)

export default Profile

You should now have a complete authentication workflow, functioning with both login and a user-restricted area!

Further reading

If you want to learn more about using production-ready auth solutions, these links may help:


Was this helpful? edit this page on GitHub