GatsbyJS authentication using Auth0

Learn how to setup Auth0 authentication with GatsbyJS V2

Fri, 02 Nov 2018

GatsbyJS authentication using Auth0

This is something that took me a while to figure due to there being a lot of small caveats. Once I knew exactly where to place all the code and what settings to use it was a breeze to setup. I’m going to make this as brief as possible and teach you how to add protected routes as well as protected components using Auth0.

Packages

Auth0 makes it convenient to get their authentication up and running. We only need one package to use their auth service

yarn add auth0-js

npm install auth0-js

Creating an Auth0 account

Head over to Auth0 to sign up for a free account. After creating your free account you should be prompted to create what is called a tenant. If you’re not prompted to create a new tenant you can click your account name at the top right and then click create tenant.

Screen Shot 2018-11-01 at 2.54.24 PM

Auth0 has a generous free tier. I wouldn’t worry about it while developing your app. It is also scalable.

Think of auth0 tenant’s as a domain. You can have multiple apps/websites under one domain and they would all be sharing the same user database. An example, I developed 3 separate applications for my company under one tenant. Person A can use her same username and password for all 3 websites due to them being under the same tenant.

You can name your tenant anything you’d like. I would name it something similar to your app or the name you’re using to develop it. Remember, you can’t change it once named.

Creating Auth Application

Now that we have our auth0 account we want to start to create a new auth application. Click Applications on the side menu and then click Create Application

Enter in any name for your application and make sure to select Single Page Web Applications. After hitting okay, you should be redirected to the application page for the app you just created. If you not you can do the following

  1. Click on Applications in the side menu
  2. Click on your application name
  3. Click on Settings

If everything worked properly you should have a page similar to this.

Screen Shot 2018-11-01 at 3.22.45 PM

I’d suggest copying your Client ID and Domain to a text editor. We will be using these later.

Now we need to add our Callback URLs in. The Callback URL is a trusted URL for your application. Auth0 redirects the client to this URL once authentication is completed. If the client is redirected to a URL that isn’t on this list the authentication will fail.

  1. Scroll further down to the section Allowed Callback URLs
  2. Since are using the default port and IP address for Gatsby, enter - http://localhost:8000/callback
  3. Scroll to the bottom of the page and click save changes

Setting up env files

We’re going to be putting all of our secrets within env files at the root of our project directory.

  1. Add .env.development to your projects root directory. The contents of the file should look similar to this.

    Auth_Callback=http://localhost:8000/callback
    Auth_Domain=**insert domain name**
    Auth_ClientId=**insert client id*
  2. Optional - You can create a similar file with .env.production using the same info as the other env file. The only difference is that you want to change your callback url to your production url.

Auth.js

Our Auth file is where all the magic happens. I’ll post the entire files contents and break it down piece by piece.

import auth0js from 'auth0-js';
import { navigate } from 'gatsby';

const isBrowser = typeof window !== 'undefined';

let profile = false;

// Only instantiate Auth0 if we’re in the browser.
const auth0 = isBrowser
  ? new auth0js.WebAuth({
    domain: process.env.Auth_Domain,
    clientID: process.env.Auth_ClientId,
    redirectUri: process.env.Auth_Callback,
    responseType: 'token id_token',
    scope: 'openid profile email',
  })
  : {};

export const Login = () => {
  if (!isBrowser) {
    return;
  }
  auth0.authorize();
};

export const Logout = () => {
  if (isBrowser) {
    localStorage.removeItem('access_token');
    localStorage.removeItem('id_token');
    localStorage.removeItem('expires_at');
    localStorage.removeItem('user');
  }

  // Remove the locally cached profile to avoid confusing errors.
  profile = false;
  navigate('/');
};

const setSession = (authResult) => {
  if (!isBrowser) {
    return;
  }

  const expiresAt = JSON.stringify(
    authResult.expiresIn * 1000 + new Date().getTime(),
  );

  localStorage.setItem('access_token', authResult.accessToken);
  localStorage.setItem('id_token', authResult.idToken);
  localStorage.setItem('expires_at', expiresAt);

  return true;
};

export const handleAuthentication = (callback) => {
  if (!isBrowser) {
    return;
  }

  auth0.parseHash((err, authResult) => {
    if (authResult && authResult.accessToken && authResult.idToken) {
      setSession(authResult);
      callback();
    } else if (err) {
      console.error(err);
    }
  });
};

export const isAuthenticated = () => {
  if (!isBrowser) {
    // For SSR, we’re never authenticated.
    return false;
  }

  const expiresAt = JSON.parse(localStorage.getItem('expires_at'));
  return new Date().getTime() < expiresAt;
};

export const getAccessToken = () => {
  if (!isBrowser) {
    return '';
  }

  return localStorage.getItem('access_token');
};

export const getUserInfo = () => new Promise((resolve, reject) => {
  // If the user has already logged in, don’t bother fetching again.
  if (profile) {
    resolve(profile.email);
  }

  const accessToken = getAccessToken();

  if (!isAuthenticated()) {
    resolve({});
    return;
  }

  auth0.client.userInfo(accessToken, (err, userProfile) => {
    if (err) {
      reject(err);
      return;
    }

    profile = userProfile;
    resolve(profile.email);
  });
});

auth0

const isBrowser = typeof window !== 'undefined';

let profile = false;

// Only instantiate Auth0 if we’re in the browser.
const auth0 = isBrowser
  ? new auth0js.WebAuth({
    domain: process.env.Auth_Domain,
    clientID: process.env.Auth_ClientId,
    redirectUri: process.env.Auth_Callback,
    responseType: 'token id_token',
    scope: 'openid profile email',
  })
  : {};

This snippet creates a new auth0 object for us to authenticate against. Before instantiating the object we make sure that we are in the browser. If we didn’t have this code Gatsby SSR would throw an error during build time.

Login/Logout

The login function starts the authentication method and the logout function removes all Auth0 authentication data from the browser and returns the user back to the page of your choosing. We will be implementing these functions into buttons/links within our app.

handleAuthentication

When the login button is pressed, auth0 will redirect the client to their authentication page. After the user puts their information in they are redirected to the callback url. The callback url will then run the handleAuthentication function and attempt to authenticate the user. If the attempt is successfully the function will attempt to set up a new session for the user.

setSession

If authentication is successful we will store the users token and expiration time in the browser.

isAuthenticated

isAuthenticated is a function that will return either true or false. If the user is currently authenticated, the return value will be true, if not it will be false. We will be using this function to protect our routes.

getUserAccessToken

This function returns the users authentication token. This is useful if you were to implement protected API routes on your backend. I have a full tutorial here that describes the topic in more depth

getUserInfo

Returns the authenticated users email address.

Before we continue

There is some code that we need to add to our gatsby-node.js file. If we don’t add this code auth0 will break during the build-html stage when you try to create a production ready version. This little bit of code caused so many hours of frustration for me. If you don’t have a gatsby-node.js file you can create one at the root of your project directory and post this code in it.

exports.onCreateWebpackConfig = ({ stage, loaders, actions }) => {
  if (stage === 'build-html') {
    /*
     * During the build step, `auth0-js` will break because it relies on
     * browser-specific APIs. Fortunately, we don’t need it during the build.
     * Using Webpack’s null loader, we’re able to effectively ignore `auth0-js`
     * during the build. (See `src/utils/auth.js` to see how we prevent this
     * from breaking the app.)
     */
    actions.setWebpackConfig({
      module: {
        rules: [
          {
            test: /auth0-js/,
            use: loaders.null(),
          },
        ],
      },
    });
  }
};

Callback Component

You want to create a new file in your src/pages directory named callback.js. The callback component will handle the applications authentication when a user attempts to log in.

import React, { Component } from 'react';
import loading from 'assets/images/loading.svg';
import { navigate } from 'gatsby';
import { handleAuthentication } from 'auth/Auth';

class Callback extends Component {
  componentDidMount() {
    handleAuthentication();
    setTimeout(() => {
      navigate('/search')
    }, 1500);
  }

  render() {
    return (
      <div style={{
        position: 'absolute',
        display: 'flex',
        justifyContent: 'center',
        height: '98vh',
        width: '98vw',
        top: 0,
        bottom: 0,
        left: 0,
        right: 0,
        backgroundColor: 'white',
      }}
      >
        <img src={loading} alt="loading" />
      </div>
    );
  }
}

export default Callback;

After a user submits the login information this component will attempt to authenticate the user. All of this is taken care of within the componentDidMount function. The function calls upon the handleAuthentication and stores the proper objects in the users local browser storage if the authentication was completed successfully.

In a regular react app we would normally have our Login function within the auth.js file do the redirecting, due to GatsbyJS nature we can’t redirect the site in the same way. We give our handleAuthentication function sometime to authenticate before we redirect the client. In this example we wait 1.5 seconds before we redirect the client.

While the client is loading we want to show them an indicator so they know everything is working as it should be. The div just centers a loading svg horizontally and vertically. You can display anything you’d like in the return statement of the component, I just think the loading image already a sufficient method. You can create your own loading svg by using this website.

Protect Components

Next we’re going to display components based on whether the user is authenticated or not. This will be a basic use showing how to protect a component. You can extend this example however you see fit using the same logic. This is our index page on our example. Put the following code inside of src/pages/index.js

import React from 'react';
import { isAuthenticated } from 'auth/Auth';
import Login from 'components/Login';
import Logout from 'components/Logout';

class IndexPage extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      authenticated: false,
    };
  }

  componentDidMount() {
    this.setState({ authenticated: isAuthenticated() });
  }

	render() {
		const { authenticated } = this.state;
		
		return (
			<div>
				{authenticated && (
            		<Logout/>
          	)}
          	{!authenticated && (
            		<Login />
          	)}
			</div>
		)
	}

}

We haven’t created our Logout and Login components yet, once we do this page will display a login button if the user isn’t authenticated or a logout button if the user is authenticated. When the component is rendered, it will run the isAuthenticated function to see if the client has been authenticated. We set the authentication status to false initially due to us not wanted to display any information until we’re sure the user is authenticated.

Login Component

This component will display a button that will start the authentication process when clicked

import React, { Component } from 'react';
import { Login } from 'auth/Auth';

class LoginButton extends Component {
  constructor(props) {
    super(props);
    this.login = this.login.bind(this);
  }

  login() {
    Login();
  }

  render() {
    return (
        <button onClick={this.login}>
        	Log In
        </button>
    );
  }
}

export default LoginButton;

Logout Component

This component will display a button that will start the logout process when clicked

import React, { Component } from 'react';
import { Logout } from 'auth/Auth';

class LogoutButton extends Component {
  constructor(props) {
    super(props);
    this.logout = this.logout.bind(this);
  }

  logout() {
    Logout();
  }

  render() {
    return (
        <button onClick={this.logout}>
        	Log In
        </button>
    );
  }
}

export default LogoutButton;

Putting it altogether

Once you create all the components, you should be able to test everything out! Clicking on the login button should take you to auth0’s authentication site for your application. Once there you can create a new account from the form that appears. Once you create the account auth0 should redirect you back to your index page. Instead of showing a button to login, now you will see the logout button.

Extending Authentication

You can extend the authentication with the following methods

Loading...
Edward Beazer

Edward Beazer - I just like to build shit. Sometimes I get stuck for hours, even days while trying to figure out how to solve an issue or implement a new feature. Hope my tips and tutorials can save you some time.