Adding Firebase Authentication in Gatsby With a Little Typescript Magic

Photo by Marvin Meyer on Unsplash

Follow me on Twitter to get this tutorial and others: @SquashBugler

Gatsby is a great framework for building and designing a website but what about authentication? Well, That’s where firebase comes in, I’ve read a few articles and posts about how to integrate firebase with GatsbyJS but most of them didn’t involve typescript support. They also failed to explain keeping a user logged in, or setting up private routes. It’s important that authentication be tracked in the browser and app state. That’s why my approach will provide further customization and help you follow security best practices, let’s get started!

Setting up the Gatsby project

I’ll be using my free Peanut butter and jelly Gatsby template for this tutorial you can check out the demo here:

Gatsby Template

And you can get it by either downloading through Gumroad or by cloning the repo.

via Gumroad: https://gum.co/pbj-template

via repo: https://github.com/JohnGrisham/PB-JPlain

Then change directories to be in the new project:

cd project-name/

Then install the project dependencies:

# using NPMnpm install# using YARNyarn

This may take a while to install all the dependencies but be patient… Once it’s done open the project in your preferred text editor, personally I use VS code, and if you’re on the fence about what you should use I highly recommend it. You should then be able to start your project by running this from the project root.

# using NPMnpm run develop# using YARNyarn develop

Open up a browser window and go to http://localhost:8000 and you should see the landing page, If you click the signup button in the header or the updates one at the bottom of the page nothing will happen.

We’ll be fixing that to allow users to signup soon.

Getting Firebase set up

Now we need to make a firebase account and add that to our project. Create a firebase account and follow this guide then come back here when you’re done.

https://firebase.google.com/docs/web/setup

You now have a firebase project in the firebase console, now to add firebase to the Gatsby project:

# using YARNyarn add firebase# using NPMnpm install firebase

Now in the firebase console go into your project settings and find your app config once you have that create an env file in the Gatsby app project root and call it .env.development this will be your development environment file where you’ll store secret or universal app information.

// .env.developmentGATSBY_FIREBASE_APIKEY=YOUR_API_KEYGATSBY_FIREBASE_AUTHDOMAIN=YOUR_AUTHDOMAINGATSBY_FIREBASE_PROJECTID=YOUR_PROJECTIDGATSBY_FIREBASE_STORAGEBUCKET=YOUR_STORAGE_BUCKETGATSBY_FIREBASE_MESSAGINGSENDERID=YOUR_MESSAGING_SENDER_IDGATSBY_FIREBASE_APPID=YOUR_APPIDGATSBY_FIREBASE_MEASUREMENTID=YOUR_MEASUREMENTID

You should be able to find all these values from the config you found earlier in the firebase project console. We’ll need to create the firebase context for our firebase instance first. So in the services folder create a firebase folder and inside it make a firebase-content.tsx file and a get-firebase-instance.ts file.

// firebase-context.tsximport * as React from 'react'import firebase from 'firebase'export interface FirebaseContextData {  isInitialized: boolean  firebase: firebase.app.App | null  authToken: string | null  setAuthToken: (authToken: string) => void}const FirebaseContext = React.createContext<FirebaseContextData>({  authToken: null,  firebase: null,  isInitialized: false,  setAuthToken: () => {}})export default FirebaseContext// get-firebase-instance.tsimport 'firebase/auth'import { checkIsClient } from '../../utils'import firebase from 'firebase'const config = {  apiKey: process.env.GATSBY_FIREBASE_APIKEY,  appId: process.env.GATSBY_FIREBASE_APPID,  authDomain: process.env.GATSBY_FIREBASE_AUTHDOMAIN,  measurementId: process.env.GATSBY_FIREBASE_MEASUREMENTID,  messagingSenderId: process.env.GATSBY_FIREBASE_MESSAGINGSENDERID,  projectId: process.env.GATSBY_FIREBASE_PROJECTID,  storageBucket: process.env.GATSBY_FIREBASE_STORAGEBUCKET}let instance: firebase.app.App | null = firebase.apps.length > 0 ? firebase.apps[0] : nullexport function getFirebase() {  if (checkIsClient()) {      if (instance) return instance      instance = firebase.initializeApp(config)      return instance  }  return null}

We’ll use both of these in our provider which will store and pass the firebase context we’ll create for use throughout the app. So go ahead and create it in the same folder.

// firebase-provider.tsximport * as React from 'react'import FirebaseContext, { FirebaseContextData } from './firebase-context'import firebase from 'firebase'import { checkIsClient } from '../../utils'import { getFirebase } from './get-firebase-instance'const FirebaseProvider: React.FC = ({ children }) => {  const isClient = React.useMemo(() => checkIsClient(), [])  const [isInitialized, setIsInitialized] = React.useState(false)  const [firebase, setFirebase] = React.useState<firebase.app.App | null> (null)  const [authToken, setAuthToken] =   React.useState<FirebaseContextData['authToken']>(isClient ? window.localStorage.getItem('authToken') : null)React.useEffect(() => {  const firebaseInstance = getFirebase()  setFirebase(firebaseInstance)    if (firebaseInstance) {     setIsInitialized(true)    }  }, [])  const onSetAuthToken = (token: string) => {    setAuthToken(token)    localStorage.setItem('authToken', token)  }React.useEffect(() => {   if (isClient && !authToken) {   const token = window.localStorage.getItem('authToken')   if (token) {    onSetAuthToken(token)    }  }}, [authToken, isClient])return (<FirebaseContext.Provider          value={{ authToken,                  firebase,                  isInitialized,                  setAuthToken: onSetAuthToken }}>                   {children}        </FirebaseContext.Provider>)}export default FirebaseProvider

This might seem complicated but it really only does a few things.

  • It initializes the firebase app
  • It sets up the context that will provide a reference to the firebase instance
  • It creates state and set state methods for tracking authentication
  • It provides the context with the firebase instance to the rest of the app

For more on contexts and how they work: https://reactjs.org/docs/context.html

Before we’re done with this section you’ll need to export the provider and context so do so in an index file at the root of services/firebase

// services/firebase/index.tsexport { default as FirebaseContext } from './firebase-context'export { default as FirebaseProvider } from './firebase-provider'

Using the firebase context with SSR

Inside the services folder, add this line to the index.ts file to export all our firebase services.

// services/index.tsexport { FirebaseContext, FirebaseProvider } from ‘./firebase’

This exports the context and provider. Then inside the gatsby-browser.js file at the root of the project add the newly created firebase provider. The final version of the file should look something like this.

// gatsby-browser.jsimport * as React from 'react'import { faReact } from '@fortawesome/free-brands-svg-icons'import { fas } from '@fortawesome/free-solid-svg-icons'import { library } from '@fortawesome/fontawesome-svg-core'import loadable from '@loadable/component'library.add(fas, faReact)const WindowProvider = loadable(() => import('./src/services/window/window-provider'))const FirebaseProvider = loadable(() => import('./src/services/firebase/firebase-provider'))export const wrapRootElement = ({ element }) =>   <FirebaseProvider>    <WindowProvider>{element}</WindowProvider>  </FirebaseProvider>

I use loadable components to dynamically import the providers and wrap them around the app’s root element to avoid any issues with server-side rendering.

You can see the docs for loadable components here:

Getting Started - Loadable Components

Signup with Firebase Authentication

We’re going to be adding a user signup flow with an auto-generated password. In order to protect the user's password and allow them to skip creating one until a more convenient time.

We’ll be using the nanoid library to do this so go ahead and install the package like so:

# using YARNyarn add nanoid# using NPMnpm install nanoid

This library will generate a unique Id that will be used as the user's password for the initial signup. We won’t know the password but neither will they so if you decide to stick with this flow make sure you send them an email later allowing them to change their password. Update the call-to-action-form.tsx file in components/call-to-action to include the signup flow.

// call-to-action-form.tsximport * as React from 'react'import * as Styled from './styles'import * as Yup from 'yup'import { Button, TextField } from '@material-ui/core'import { Formik, FormikHelpers } from 'formik'import { FirebaseContext } from '../../services'import { nanoid } from 'nanoid'interface CTAValue {  email: string}const formSchema = Yup.object().shape({email: Yup.string().email('Please enter a valid email.').required('Email is required.')})const CallToActionForm: React.FC = () => {   const { authToken, firebase, setAuthToken } =   React.useContext(FirebaseContext)const onSubmitCTA = React.useCallback(async ({ email }: CTAValue, actions: FormikHelpers<CTAValue>) => {    try {    if (!firebase) {    return  }   const password = nanoid()   const { user } = await   firebase.auth().createUserWithEmailAndPassword(email, password)     if (user) {        const { refreshToken } = user        setAuthToken(refreshToken)     }        actions.setStatus({ errors: [], success: true })      } catch (error) {        actions.setStatus({ errors: [error] })      } finally {       actions.setSubmitting(false)  }}, [firebase, setAuthToken])return (<Formik<CTAValue>        initialValues={{ email: '' }}        initialStatus={{ errors: [], success: false }}        onSubmit={onSubmitCTA}        validationSchema={formSchema}>        {({ errors,            handleBlur,            handleChange,            handleSubmit,            isSubmitting,            isValid,            setStatus,            status,            touched }) => (<Styled.CallToActionForm onSubmit={handleSubmit}>    <Styled.CallToActionInput>   <TextField id="CTA" error={touched && errors?.email &&   !status.success ? true : false}helperText={touched && status.errors.length <= 0 && !status.success && errors?.email ? errors.email : undefined}label="Email"name="email"type="email"onChange={(e) => { setStatus({ errors: [], success: false })  handleChange(e)}}onBlur={handleBlur}variant="outlined"required />  <Button  disabled={!!authToken || !isValid || isSubmitting}  type="submit"  variant="contained"  color="primary">Yummy Updates</Button>{status.errors.length > 0 && status.errors.map((error: { message:  string }, i: number) => (<Styled.CallToActionError key={`cta-error-${i}`}>{error.message}</Styled.CallToActionError>))}{status.errors.length <= 0 && (status.success || !!authToken) &&   (<Styled.CallToActionSuccess>  Congratulations you&apos;re now on the waiting list!   </Styled.CallToActionSuccess>)}  </Styled.CallToActionInput>  </Styled.CallToActionForm>)}</Formik>)}export default CallToActionForm

The form component will handle sign up authentication, it will also validate the email and handle any errors!

Before all of this will work though you’ll have to enable the sign-in option from the firebase project console; there's a brief explanation of how to do this in the firebase documentation.

Authenticate with Firebase using Password-Based Accounts using Javascript

If you did everything right when you signup at the bottom of the page you should get a ‘Congratulations you’re now on the waiting list!’ message and a new user in your firebase authentication records. In addition, the user shouldn’t be able to sign up again during this session since we track the auth token in state and storage.

Note: This form only gets used in the updates section at the bottom of the template landing page. But you can use the sign-in logic in the header to handle signing up that way. In the $5 version of this template, I add a conversion context to prompt users to sign up if they haven’t already.

Private Routes

Now, in components create a private folder and inside it add private-route.tsx and index.ts files. We will use this component to prevent unauthenticated users from seeing content that they shouldn’t be allowed to access.

// private-route.tsximport { FirebaseContext } from '../../services'import React from 'react'import { checkIsClient } from '../../utils'import { navigate } from 'gatsby'export interface PrivateRouteProps {  path?: string}const PrivateRoute: React.FC<PrivateRouteProps> = ({ children, path = '/' }) => {const { authToken } = React.useContext(FirebaseContext)const isClient = React.useMemo(() => checkIsClient(), [])React.useEffect(() => {  const checkPermission = () => {        if (!authToken && window.location.href !== path) {         navigate(path)       }   }    if (!isClient) {         return       }        checkPermission()   }, [authToken, isClient, path])   if (!authToken) {       return null   }       return <>{children}</>}export default PrivateRoute// components/private/index.tsexport { default as PrivateRoute } from './private-route'

This will re-route users to the landing page if they try to access anything that is nested in this component. Feel free to create another page to test it out. (the free template only has the landing page) You’ll still have to implement signing out but all that involves is making the token expire somehow.

Conclusion

That concludes this tutorial on GatsbyJS and firebase, this solution is an improvement on some of the other tutorials I’ve seen that don’t use typescript or store the firebase instance in state. By tracking the authToken we get more control over the state and we can easily add new fields to our context.

I hope you’ve learned something from this article and if you have any questions feel free to leave a comment below I will be writing more articles in the future on other topics that I feel haven’t been covered well enough or that I struggled with, thanks for joining me!

If you got lost or just want to save some time you can download the $5 template which will include this code and more!

https://gum.co/pbj-template

You can also check out the landing page where I use this template here: https://echo-breaking-news.com/

Edit: 12/11/2020

There is some additional setup I had to do to get firebase to function properly with Gatsby when building and deploying my project. You will have to exclude building firebase packages when running Gatsby build. Create a gatsby-node.js file in the root of your project and include this code.

// gatsby-node.jsexports.onCreateWebpackConfig = ({ stage, actions, getConfig }) =>      {if (stage === 'build-html')   {actions.setWebpackConfig({externals: getConfig().externals.concat(function (context, request,  callback) {const regex = /^@?firebase(\/(.+))?/// exclude firebase products from being bundled, so they will be loaded using require() at runtime.if (regex.test(request)) {    return callback(null, 'umd ' + request)}   callback()      })    })  }}

This will prevent gatsby from including firebase packages and giving you an error. Some Firebase products include references to the window object or other objects that won’t be available at build time so so we’ll just require those when we actually run the app.

Powered By Swish