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.
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/*`] },
},
]
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;
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);
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;
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.
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
Top comments (0)