Protect GatsbyJS with Auth0 for Single Page Apps

TL;DR: Add Auth0 SPA authentication to your GatsbyJS app in minutes. See the finished project at the craigphares/gatsby-auth0 repository on GitHub.

Often, you need to create an app with gated content, restricted to only authenticated users. Fortunately, with the new Auth0 Single Page App SDK and the GatsbyJS framework, you can add serious security to your app with minimal effort.

GatsbyJS is a super popular framework built on React that helps you build blazing fast Single Page Applications (SPA).

Auth0 provides a secure login infrastructure to authenticate users for your apps.

This tutorial builds upon the Gatsby Authentication Tutorial, the Auth0 React Quickstart, and Securing Gatsby with Auth0 by Sam Julien. Using a combination of tips from each article, I’ve been able to piece together an effective way to integrate the new Auth0 SPA SDK into Gatsby.

Prerequisites

This article requires that you have Node.js with NPM installed. Make sure to download and install it before continuing. It also assumes you’re using Yarn for package management, but feel free to use NPM as needed if you don’t have it.

You will also need the gatsby-cli. Install it globally via NPM with the following command:

npm install -g gatsby-cli

Getting started with Gatsby

Start by creating a new Gatsby project with the simple hello-world starter, and navigating to its root folder:

gatsby new gatsby-auth0 gatsbyjs/gatsby-starter-hello-world
cd gatsby-auth0

In this tutorial, I’m calling the project gatsby-auth0 but feel free to name it whatever you like. Go ahead and open the project in your favorite editor.

To begin, let’s add a NavBar component to hold our links:

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

const NavBar = () => (
  <nav>
    <Link to="/">Home</Link>
    {` `}
    <Link to="/account/profile">Profile</Link>
  </nav>
)

export default NavBar

This is just a simple React component that uses Gatsby’s own Link components to navigate to different destinations. Later, we’re going to protect the Profile section, and hook up the login/logout logic.

Make a Layout component to wrap all our app elements:

// src/components/Layout.js
import React from "react"
import NavBar from "./NavBar"

const Layout = ({ children }) => (
  <>
    <NavBar />
    {children}
  </>
)

export default Layout

Then, update the index page to use our new layout:

// src/pages/index.js
import React from "react"
import Layout from "../components/Layout"

export default function Home() {
  return (
    <Layout>
      <h1>Hello world!</h1>
    </Layout>
  )
}

And create a placeholder Profile component to be filled in later:

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

const Profile = () => {
  return (
    <>
      <h1>Your profile</h1>
      <dl>
        <dt>Name</dt>
        <dd>Your name will appear here</dd>

        <dt>Email</dt>
        <dd>Your email will go here</dd>
      </dl>
    </>
  )
}

export default Profile

Create client-only routes

Because our profile section should require a logged in user, we need to define restricted content in our app. Let’s assume everything that starts with /account/ is restricted. Create a gatsby-node.js file in your project root:

// gatsby-node.js
exports.onCreatePage = async ({ page, actions }) => {
  const { createPage } = actions
  
  if (page.path.match(/^\/account/)) {
    page.matchPath = "/account/*"
    createPage(page)
  }
}

This tells Gatsby that anything under the /account/ directory should be created on-demand, instead of during build time with Gatsby client-only routes.

Then, create a generic Account component to hold all our secure routes:

// src/pages/account.js
import React from "react"
import { Router } from "@reach/router"
import Layout from "../components/Layout"
import Profile from "../components/Profile"

const Account = () => (
  <Layout>
    <Router>
      <Profile path="/account/profile" />
    </Router>
  </Layout>
)

export default Account

At this point, you can run gatsby develop and navigate to http://localhost:8000 to see your basic site with links to the home page and profile page.

Configure Auth0

If you haven’t already, sign up for an Auth0 account, and create a new application from the “Applications” section in your Auth0 dashboard.

Name your new application, select “Single Page Application” for the Application Type, and then go to Settings. Set Allowed Callback URLs, Allowed Logout URLs, and Allowed Web Origins to http://localhost:8000, then click “Save Changes.” This tells Auth0 that it can use your local development server for authentication. When you migrate to a production server, you can add those URLs as well.

Install Auth0

Use Yarn to install the Auth0 JavaScript SDK for Single Page Applications:

yarn add @auth0/auth0-spa-js

Now add the Auth0 React wrapper from the Auth0 Quickstart to Gatsby:

// src/services/auth.js
import React, { useState, useEffect, useContext } from "react"
import createAuth0Client from "@auth0/auth0-spa-js"

const DEFAULT_REDIRECT_CALLBACK = () =>
  window.history.replaceState({}, document.title, window.location.pathname)

export const Auth0Context = React.createContext()
export const useAuth0 = () => useContext(Auth0Context)
export const Auth0Provider = ({
  children,
  onRedirectCallback = DEFAULT_REDIRECT_CALLBACK,
  ...initOptions
}) => {
  const [isAuthenticated, setIsAuthenticated] = useState()
  const [user, setUser] = useState();
  const [auth0Client, setAuth0] = useState();
  const [loading, setLoading] = useState(true);
  const [popupOpen, setPopupOpen] = useState(false);

  useEffect(() => {
    const initAuth0 = async () => {
      const auth0FromHook = await createAuth0Client(initOptions)
      setAuth0(auth0FromHook)

      if (window.location.search.includes("code=") &&
          window.location.search.includes("state=")) {
        const { appState } = await auth0FromHook.handleRedirectCallback()
        onRedirectCallback(appState)
      }

      const isAuthenticated = await auth0FromHook.isAuthenticated()

      setIsAuthenticated(isAuthenticated)

      if (isAuthenticated) {
        const user = await auth0FromHook.getUser()
        setUser(user)
      }

      setLoading(false)
    }
    initAuth0()
    // eslint-disable-next-line
  }, [])

  const loginWithPopup = async (params = {}) => {
    setPopupOpen(true)
    try {
      await auth0Client.loginWithPopup(params)
    } catch (error) {
      console.error(error)
    } finally {
      setPopupOpen(false)
    }
    const user = await auth0Client.getUser()
    setUser(user)
    setIsAuthenticated(true)
  };

  const handleRedirectCallback = async () => {
    setLoading(true)
    await auth0Client.handleRedirectCallback()
    const user = await auth0Client.getUser()
    setLoading(false)
    setIsAuthenticated(true)
    setUser(user)
  }
  return (
    <Auth0Context.Provider
      value={{
        isAuthenticated,
        user,
        loading,
        popupOpen,
        loginWithPopup,
        handleRedirectCallback,
        getIdTokenClaims: (...p) => auth0Client.getIdTokenClaims(...p),
        loginWithRedirect: (...p) => auth0Client.loginWithRedirect(...p),
        getTokenSilently: (...p) => auth0Client.getTokenSilently(...p),
        getTokenWithPopup: (...p) => auth0Client.getTokenWithPopup(...p),
        logout: (...p) => auth0Client.logout(...p)
      }}
    >
      {children}
    </Auth0Context.Provider>
  )
}

This is a set of custom React hooks that simplify how you work with the Auth0 SDK, providing functions that allow the user to log in, log out, and information such as whether the user is currently authenticated.

Update the navigation bar

Let’s add some logic to the NavBar, and implement the login and logout hooks:

// src/components/NavBar.js
import React from "react"
import { Link } from "gatsby"
import { useAuth0 } from "../services/auth"

const NavBar = () => {
  const { isAuthenticated, loginWithRedirect, logout } = useAuth0()

  return (
    <nav>
      <Link to="/">Home</Link>
      {` `}
      {isAuthenticated &&
        <Link to="/account/profile">Profile</Link>
      }
      {` `}

      {!isAuthenticated && (
        <button onClick={() => loginWithRedirect({})}>Log in</button>
      )}

      {isAuthenticated && <button onClick={() => logout()}>Log out</button>}
    </nav>
  )
}

export default NavBar

We determine if the user is logged in with the isAuthenticated property.

Wrap the Auth0Provider

In order for the authentication system to work properly, our app components need to be wrapped in the Auth0Provider component provided by our SDK wrapper. Any component inside this will have access to the Auth0 SDK client.

Add a root App component:

// src/components/App.js
import React from "react"
import { Auth0Provider } from "../services/auth"

const App = ({ element, location }) => {

  const onRedirectCallback = appState => {
    location.navigate(
      appState && appState.targetUrl
        ? appState.targetUrl
        : window.location.pathname
    )
  }

  return (
    <Auth0Provider
      domain={process.env.AUTH0_DOMAIN}
      client_id={process.env.AUTH0_CLIENT_ID}
      redirect_uri={window.location.origin}
      cacheLocation='localstorage'
      onRedirectCallback={onRedirectCallback}
    >
      {element}
    </Auth0Provider>
  )
}

export default App

Here we pass in the Domain and Client ID from environment variables and we set the cacheLocation to 'localstorage' to make sure our authenticated status persists between page reloads.

Add a dotenv file at the root called .env.development to hold our Auth0 Domain and Client ID in development:

# .env.development
AUTH0_DOMAIN=YOUR_DOMAIN
AUTH0_CLIENT_ID=YOUR_CLIENT_ID

Make sure to replace YOUR_DOMAIN with your Auth0 Domain and YOUR_CLIENT_ID with your Auth0 Client ID from your Auth0 Application settings. You can add a .env.production file with production settings if desired.

In order to tell Gatsby we need to wrap our app with our App component, we need to use Gatsby Browser APIs. Create a gatsby-browser.js file in the root of your project:

// gatsby-browser.js
import React from "react"
import { Location } from "@reach/router"
import App from "./src/components/App"

export const wrapRootElement = ({ element }) => {
  return (
    <Location>
      {location => <App element={element} location={location} />}
    </Location>
  )
}

If you’re planning on using Server-Side Rendering (SSR) in Gatsby, you’ll also want to add this to gatsby-ssr.js.

We need to wait for Auth0 to finish loading before we can access its SDK. Edit the Layout component to add some loading logic:

// src/components/Layout.js
import React from "react"
import NavBar from "./NavBar"
import { useAuth0 } from "../services/auth"

const Layout = ({ children }) => {
  const { loading } = useAuth0()

  if (loading) {
    return <div>Loading...</div>
  }

  return (
    <>
      <NavBar />
      {children}
    </>
  )
}

export default Layout

You can now restart the Gatsby app and navigate to http://localhost:8000 to test your login flow. When you click the Log in button, you’ll be taken to the Auth0 Universal Login experience. Sign up or log in with your credentials, and you’ll be taken back to the app with the profile link and a Log out button. Log out, and you’re back to the logged out version of the home page.

Read the user profile

Once you’re logged in, the user information can be retrieved from Auth0. Let’s update the profile page to fetch and display this info:

// src/components/Profile.js
import React from "react"
import { useAuth0 } from "../services/auth";

const Profile = () => {
  const { loading, user } = useAuth0();

  if (loading || !user) {
    return <div>Loading...</div>;
  }

  return(
    <>
      <h1>Your profile</h1>
      <img src={user.picture} alt="Profile" />
      <dl>
        <dt>Name</dt>
        <dd>{user.name}</dd>

        <dt>Email</dt>
        <dd>{user.email}</dd>
      </dl>
    </>
  )
}

export default Profile

Here you can see that we wait for the user to load, then display the picture, name, and email from the Auth0 account.

Secure the profile page

We need to make sure someone who is not logged in can’t navigate directly to the profile URL. In order to achieve this, we need to wrap the profile route in a higher order component that takes the authentication state into account.

Let’s make a PrivateRoute component:

// src/components/PrivateRoute.js
import React, { useEffect } from "react"
import { useAuth0 } from "../services/auth"

const PrivateRoute = ({ component: Component, path, ...rest }) => {
  const { loading, isAuthenticated, loginWithRedirect } = useAuth0()

  useEffect(() => {
    if (loading || isAuthenticated) {
      return
    }
    const fn = async () => {
      await loginWithRedirect({
        appState: {targetUrl: window.location.pathname}
      })
    }
    fn()
  }, [loading, isAuthenticated, loginWithRedirect, path])

  return isAuthenticated === true ? <Component {...rest} /> : null
}

export default PrivateRoute

This component will check if the user is authenticated, and start the login flow if required. Now we can use this in our account page:

// src/pages/acccount.js
import React from "react"
import { Router } from "@reach/router"
import Layout from "../components/Layout"
import Profile from "../components/Profile"
import PrivateRoute from "../components/PrivateRoute"

const Account = () => (
  <Layout>
    <Router>
      <PrivateRoute path="/account/profile" component={Profile} />
    </Router>
  </Layout>
)

export default Account

Run the project again. If you’re not authenticated, and you navigate to http://localhost:8000/account/profile in your browser address bar, you’ll be prompted to log in, and taken to the profile page when you return.

Conclusion

You now have a complete authentication workflow with user-restricted content. Download the finished source code at craigphares/gatsby-auth0. This approach can be applied to any Gatsby app. Add some content and some CSS styles, and you’re good to go!

Thanks for reading, and keep making! 🚀