Part 3: Authentication and private routes in Gatsby

klekanger

Kurt Lekanger

Posted on September 12, 2021

Part 3: Authentication and private routes in Gatsby

In part 1 and 2 of this series I described the technology choices I made before starting to build new web pages for my local condominium. I also went through how I configured Gatsby on the frontend and Contentful on the backend.

Gatsby is often referred to as a "static site generator", which means that when you enter the command gatsby build, Gatsby starts retrieving content from a CMS, an API or perhaps markdown files on the disk. Content and data from different sources are combined, and Gatsby renders static HTML files and packs everything together - without you having to know anything about Webpack configuration, code splitting or other things that can often be a bit complicated to set up.

Great performance is one of the many benefits of static web sites. Static sites are also secure. Because the web pages are created as you build the page, and the user is served static HTML pages, the attack surface is significantly reduced. For example, it is not possible for an attacker to access content from databases or your CMS, other than the content Gatsby already has retrieved when the static pages were generated.

Gatsby does not have to be just static pages

As mentioned in the first parts of this series, I wanted to have a separate area on the website that would only be available to our residents (behind a login page). These pages should not be static, but fetch content dynamically as needed, in my case depending on whether the user is logged in or not.

Before I go into how I made the login functionality, I want to talk about how Gatsby can handle pages that are only available to logged in users.

Gatsby supports so-called client-only routes. This makes it possible to create pages that exist only on the client (in the browser) and where static HTML pages are not created in the /public folder when you run the gatsby build command. Client-only routes work more like a traditional single page app in React, and by using Reach Router which is built into Gatsby, you can handle the various routes that only logged-in users should see.

For the user login, we need an authentication solution. I didn't want to build this myself, so I chose Auth0. This is a well recognized and proven solution with a lot of features I will need when building a dashboard for user administration. Using Auth0, I can protect access to all client-only routers.

Below is a simplified diagram that shows how this works on my web site. The blue boxes are static pages created when building the Gatsby site. For the route /information, a static page is also created which, if the user is not logged in, shows a message informing you that you must log in to see the content. If the user is logged in, Reach Router is used to display the correct React component depending on which route the user is trying to reach. This is wrapped in a <Privateroute> component that uses a higher order component in auth0-react called withAutenthicationRequired to check if the user is logged in or not.

A PrivateRoute component works as a "gate keeper", letting through logged in users only.
A PrivateRoute component works as a "gate keeper", letting through logged in users only.

To simplify the process of making client-only routes, I use an official Gatsby plugin called gatsby-plugin-create-client-paths. When you have installed this plugin, you can edit gatsby-config.js to configure which routes you want to be private (Gatsby will not create static pages out of these):

// ./gatsby-config.js

plugins: [
{
      resolve: `gatsby-plugin-create-client-paths`,
      options: { prefixes: [`/informasjon/*`, `/min-side/*`] },
},
]
Enter fullscreen mode Exit fullscreen mode

In the code example above, every path (url) ending in /informasjon and /min-side ("My page" in Norwegian) will not be static pages, but render the routes I have set up in src/pages/informasjon.tsx or src/pages/min-side.tsx. On the condominium's website, there is a menu item on the navigation bar called For residents that navigates to https://gartnerihagen-askim.no/informasjon. To create this client-only route in Gatsby, I created the file src/pages/informasjon.tsx and used Reach Router to display different React components depending on the route. For example, if the user visits the web page on the route /informasjon/dokumenter, the <Dokumenter> component should be displayed.

This is my informasjon.tsx page, and how the routing is set up (abbreviated, see complete source code at https://github.com/klekanger/gartnerihagen):

// ./src/pages/informasjon.tsx

import * as React from 'react';
import { useAuth0 } from '@auth0/auth0-react';
import { Router } from '@reach/router';
import PrivateRoute from '../utils/privateRoute';

import InfoPage from '../components/private-components/informasjon';
import Referater from '../components/private-components/referater';

import LoadingSpinner from '../components/loading-spinner';
import NotLoggedIn from '../components/private-components/notLoggedIn';

const Informasjon = () => {
  const { isLoading, isAuthenticated, error } = useAuth0();

  if (isLoading) {
    return (
      <Box>
        <LoadingSpinner spinnerMessage='Autentiserer bruker' />
      </Box>
    );
  }

  if (error) {
    return <div>Det har oppstått en feil... {error.message}</div>;
  }

  if (!isAuthenticated) {
    return <NotLoggedIn />;
  }

  return (
    <Router>
      <PrivateRoute path='/informasjon' component={InfoPage} />
      <PrivateRoute
        path='/informasjon/referater/'
        component={Referater}
        title='Referater fra årsmøter'
        excerpt='På denne siden finner du referater fra alle tidligere årsmøter. Er det noe du savner, ta kontakt med styret.'
      />
    </Router>
  );
};

export default Informasjon;
Enter fullscreen mode Exit fullscreen mode

My <PrivateRoute> component looks like the code snippet below. This component ensures that the user must be logged in to get access. If not, the user will get Auth0's authentication popup:

// ./src/utils/privateRoute.tsx

import * as React from 'react';
import { withAuthenticationRequired } from '@auth0/auth0-react';

interface IPrivateroute {
  component: any;
  location?: string;
  path: string;
  postData?: any;
  title?: string;
  excerpt?: string;
}

function PrivateRoute({ component: Component, ...rest }: IPrivateroute) {
  return <Component {...rest} />;
}

export default withAuthenticationRequired(PrivateRoute);
Enter fullscreen mode Exit fullscreen mode

Navbar with login

As mentioned, we need an authentication solution to find out who should have access and who should not. The first version of the condominium's website was set up with Netlify Identity and Netlify Identity Widget, a solution that was very easy to configure.

However, it soon became apparent that Netlify Identity had some limitations. One was that the login alert was not in Norwegian (I translated it and opened a pull request, but could not wait for it to go through. It's been 7 months now...). The other reason for not sticking with Netlify Identify was that I started working on a dashboard for user account management where I would need some more advanced functionality than Netlify Identity Widget could provide. After some research, I ended up choosing Auth0.

After registering and setting up everything at Auth0.com, I installed the Auth0 React SDK with: npm install @auth0/auth0-react

Auth0 React SDK uses React Context, so you can wrap your entire application in an Auth0Provider so that Auth0 knows whether the user is logged in or not, no matter where in the application the user is. When your application is wrapped in Auth0Provider, you can in any component import the useAuth hook like this: import { useAuth0 } from '@auth0/auth0-react' and from useAuth retrieve various methods or properties that have to do with login, for example check if the user is authenticated, bring up a login box, etc. Example: const { isAuthenticated } = useAuth0() makes it easy to later check if the user is logged in by doing this: if (!isAuthenticated) { return <NotLoggedIn /> }

So how do we wrap our application in Auth0Provider? It's quite straightforward: In Gatsby you can wrap the root element of the web page with another component by exporting wrapRootElement from the gatsby-browser.js file. Read more about it in the Gatsby documentation.

This is what my gatsby-browser.js file looks like, with Auth0Provider set up so that all pages on the webpage have access to information about whether the user is logged in or not:

// ./gatsby-browser.js

import * as React from 'react';
import { wrapPageElement as wrap } from './src/chakra-wrapper';
import { Auth0Provider } from '@auth0/auth0-react';
import { navigate } from 'gatsby';

const onRedirectCallback = (appState) => {
  // Use Gatsby's navigate method to replace the url
  navigate(appState?.returnTo || '/', { replace: true });
};

export const wrapRootElement = ({ element }) => (
  <Auth0Provider
    domain={process.env.GATSBY_AUTH0_DOMAIN}
    clientId={process.env.GATSBY_AUTH0_CLIENT_ID}
    redirectUri={window.location.origin}
    onRedirectCallback={onRedirectCallback}
  >
    {element}
  </Auth0Provider>
);

export const wrapPageElement = wrap;
Enter fullscreen mode Exit fullscreen mode

I created a login button in the navigation bar at the top of the web page. When the user tries to log in, he or she is sent to Auth0's login page - and redirected to the condominium's website if the username and password are correct.

The login button also gives access to a My page ("Min Side") where the user can see information about who is logged in, and has the opportunity to change passwords. For security reasons, the password is not changed directly, but instead the Change Password button will send a POST request to Auth0's authentication API with a request to change the password. Auth0 has a description of how this works here.

Screenshot: The user has access to "My Page" under the login button when logged in.

The user has access to "My Page" under the login button when logged in.

Securing the content

In the original project I used Gatsby's GraphQL data layer to fetch content for the protected routes, using Gatsby's useStaticQuery hook. That meant that all the content was fetched during build time - even the content that should be accessible to logged in users only. The users could not access these protected routes without being authenticated, but technical users could find private content via the network tab in the browsers dev tools.

To prevent this, I had to rewrite the components used in client-only routes to use Apollo Client in stead of Gatsbys GraphQL data layer for fetching data. Data that should be available on the client only at run-time are fetched from the Contentful GraphQL Content API (and not via the build-time gatsby-source-contentful plugin) using Apollo Client.

To get this to work I had to make changes in both how rich text was handled (since it was different depending on whether I used gatsby-source-contentful or retrieved the content dynamically from Contentfuls GraphQL content API). I also had to build a custom component for handling images delivered from the Contentfuls Image API, since I could not use Gatsby Image with Contentful's own API. I wanted the same performance as with Gatsby Image, and the images delivered in "correct" sizes depending on screen width. I won't get into all the details, but you can find the complete source code at my Github here, and my custom image component here.

In the next part of this series, I will go through how I deployed the final web site to Netlify, using continous deployment.

In the two final parts of the series, I will show how I built the user admin dashboard that let's administrators create or update the users that should have access to the protected routes of our web page.

Next step: Setting up continous deployment to Netlify

Feel free to take a look at the finished website here: https://gartnerihagen-askim.no

The project is open source, you can find the source code at my Github.

This is a translation, the original article in Norwegian is here: Del 3: Slik bygget jeg sameiets nye nettsider. Autentisering og private ruter i Gatsby

💖 💪 🙅 🚩
klekanger
Kurt Lekanger

Posted on September 12, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related