DEV Community

Cover image for Adding Firebase Authentication in Gatsby With a Little Typescript Magic
John Grisham
John Grisham

Posted on • Edited on • Originally published at Medium

Adding Firebase Authentication in Gatsby With a Little Typescript Magic

To support me please read this tutorial at its original posting location on Medium:
Setup Gatsby Firebase Authentication with Typescript in 10 minutes



    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

First, you'll want to add the Gatsby client globally, use either depending on preference but I prefer yarn:

# using YARN
yarn global add gatsby-cli
# using NPM
npm install gatsby-cli -g
Enter fullscreen mode Exit fullscreen mode

And then create the new project with gatsby-cli:

gatsby new project-name https://github.com/resir014/gatsby-starter-typescript-plus
Enter fullscreen mode Exit fullscreen mode

There are a few different forks of the original gatsby-default starter but I chose this one because it had a decent number of stars and forks.

Then change directories to be in the new project:

cd project-name/
Enter fullscreen mode Exit fullscreen mode

Then install the project dependencies:

# using NPM
npm install
# using YARN
yarn
Enter fullscreen mode Exit fullscreen mode

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 NPM
npm start
# using YARN
yarn start
Enter fullscreen mode Exit fullscreen mode

Open up a browser window and go to http://localhost:8000 and you should see a basic landing page, fancy!

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 YARN
yarn add firebase
# using NPM
npm install firebase
Enter fullscreen mode Exit fullscreen mode

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.development

GATSBY_FIREBASE_APIKEY={YOUR_API_KEY}
GATSBY_FIREBASE_AUTHDOMAIN={YOUR_AUTHDOMAIN}
GATSBY_FIREBASE_DATABASEURL={YOUR_DATABASE_URL}
GATSBY_FIREBASE_PROJECTID={YOUR_PROJECTID}
GATSBY_FIREBASE_STORAGEBUCKET={YOUR_STORAGE_BUCKET}
GATSBY_FIREBASE_MESSAGINGSENDERID={YOUR_MESSAGING_SENDER_ID}
GATSBY_FIREBASE_APPID={YOUR_APPID}
GATSBY_FIREBASE_MEASUREMENTID={YOUR_MEASUREMENTID}
Enter fullscreen mode Exit fullscreen mode

You should be able to find all these values from the config you found earlier in the firebase project console. Now in the src folder add a services folder and create a firebase provider file called FirebaseProvider.tsx this will be our firebase provider that will store and pass the firebase context we'll create for use throughout the app.


    // FirebaseProvider.tsx

    import React from 'react'
    import firebase from 'firebase'

    // Your config that you stored in the env file.
    const firebaseConfig = {
     apiKey: process.env.GATSBY_FIREBASE_APIKEY,
     appId: process.env.GATSBY_FIREBASE_APPID,
     authDomain: process.env.GATSBY_FIREBASE_AUTHDOMAIN,
     databaseURL: process.env.GATSBY_FIREBASE_DATABASEURL,
     measurementId: process.env.GATSBY_FIREBASE_MEASUREMENTID,
     messagingSenderId: process.env.GATSBY_FIREBASE_MESSAGINGSENDERID,
     projectId: process.env.GATSBY_FIREBASE_PROJECTID,
     storageBucket: process.env.GATSBY_FIREBASE_STORAGEBUCKET
    }
    // The type definition for the firebase context data.

    export interface FirebaseContextData {
     isInitialized: boolean
     firebase: typeof firebase
     authToken: string | null
     setAuthToken: (authToken: string) => void
    }
    // The firebase context that will store the firebase instance and other useful variables.

    export const FirebaseContext = React.createContext<FirebaseContextData>({
     authToken: null,
     firebase,
     isInitialized: false,
     setAuthToken: () => {}
    })

    // The provider that will store the logic for manipulating the firebase instance and variables.

    export const FirebaseProvider: React.FC = ({ children }) => {
     const [isInitialized, setIsInitialized] = React.useState(false)

    // If we have a window and the authToken already exists in localstorage then initialize the authToken value otherwise null.

    const [authToken, setAuthToken] = React.useState<FirebaseContextData['authToken']>(
     typeof window === 'object' ? window.localStorage.getItem('authToken') : null
     )

     // If firebase has not been initialized then initialize it.
     if (!firebase.apps.length) {
     firebase.initializeApp(firebaseConfig)
     setIsInitialized(true)
     }

     // A method for setting the authToken in state and local storage.
     const onSetAuthToken = (token: string) => {
     setAuthToken(token)
     localStorage.setItem('authToken', token)
     }

     // If we have the window object and there is no authToken then try to get the authToken from local storage.
     React.useEffect(() => {
     if (typeof window === 'object' && !authToken) {
     const token = window.localStorage.getItem('authToken')

       if (token) {
         onSetAuthToken(token)
         }
       }
    }, [authToken])

    return (
     <FirebaseContext.Provider
     value={{
     authToken,
     firebase,
     isInitialized,
     setAuthToken: onSetAuthToken
     }}>
     {children}
     </FirebaseContext.Provider>
     )
    }

Enter fullscreen mode Exit fullscreen mode

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

Using the firebase context

Inside the services folder create an index.ts file that will export all of our services.

// index.ts

export { FirebaseContext, FirebaseProvider } from './FirebaseProvider'
Enter fullscreen mode Exit fullscreen mode

This exports the context and provider. Then inside the components folder find the LayoutRoot.tsx file and wrap the provider around it.

// LayoutRoot.tsx

import * as React from 'react'
import { Global, css } from '@emotion/core'
import { FirebaseProvider } from '../services'
import styled from '@emotion/styled'
import normalize from '../styles/normalize'

const StyledLayoutRoot = styled.div`
 display: flex;
 flex-direction: column;
 min-height: 100vh;
`
interface LayoutRootProps {
 className?: string
}

const LayoutRoot: React.FC<LayoutRootProps> = ({ children, className }) => (
 <FirebaseProvider>
 <Global styles={() => css(normalize)} />
 <StyledLayoutRoot className={className}>{children}</StyledLayoutRoot>
 </FirebaseProvider>
)

export default LayoutRoot
Enter fullscreen mode Exit fullscreen mode

This will provide our context to the rest of the app. Also in the services folder create a types folder and inside that create a file called AuthError.tsx that will export the error type we'll need in a minute.

// AuthError.tsx

import { FirebaseError as FBError } from 'firebase'
export type AuthError = FBError & Error
Enter fullscreen mode Exit fullscreen mode

And then export that from the root of services like so.

In the index.ts file of the services folder:

// index.ts

export { FirebaseContext, FirebaseProvider } from './FirebaseProvider'
export { AuthError } from './types/AuthError'
Enter fullscreen mode Exit fullscreen mode

This is an error type that will come in handy when catching errors that we might get from firebase authentication. Now find the Page component in src/components/Page.tsx and make some changes.

// Page.tsx

import * as React from 'react'
import { FirebaseContext } from '../services'
import styled from '@emotion/styled'
import { dimensions } from '../styles/variables'

const StyledPage = styled.div`
 display: block;
 flex: 1;
 position: relative;
 padding: ${dimensions.containerPadding}rem;
 margin-bottom: 3rem;
`

interface PageProps {
 className?: string
}

const Page: React.FC<PageProps> = ({ children, className }) => { 
 const { isInitialized } = React.useContext(FirebaseContext)
 console.log(`firebase instance is ${isInitialized ? 'initialized' : 'not initialized'}`)

 return(<StyledPage className={className}>{children}</StyledPage>)
}

export default Page
Enter fullscreen mode Exit fullscreen mode

You may have to refresh the page but you should get a message logged in the console that says the instance has been initialized. You can go ahead and remove these changes if it worked. Now we will add some more dependencies for creating the login page.

Making it stylish! The login page edition

# using YARN
yarn add @material-ui/core @fortawesome/react-fontawesome @fortawesome/free-brands-svg-icons
# using NPM
npm install @material-ui/core @fortawesome/react-fontawesome @fortawesome/free-brands-svg-icons
Enter fullscreen mode Exit fullscreen mode

We'll need material and font awesome to quickly make the login page. In components create a Login.tsx file that will handle our login logic.


    // Login.tsx

    import { AuthError, FirebaseContext } from '../services'
    import { Button, FormControl, FormHelperText, Input, InputLabel } from '@material-ui/core'
    import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
    import React from 'react'
    import { auth } from 'firebase'
    import { faGoogle } from '@fortawesome/free-brands-svg-icons'
    import { navigate } from 'gatsby'

    const Login: React.FC = () => {
     // get the variables we need for authentication.
     const { firebase, authToken, setAuthToken } = React.useContext(FirebaseContext)
     // setup some state variables for login
     const [email, setEmail] = React.useState<string>('')
     const [password, setPassword] = React.useState<string>('')

     // The method for handling google authentication
     const handleGoogleAuth = React.useCallback(
         async (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
     try {
        event.preventDefault()
        const provider = new firebase.auth.GoogleAuthProvider()
    // get the credential from the google auth.
        const { credential } = await   firebase.auth().signInWithPopup(provider)
     // if we have a credential then get the access token and set it in state.

        if (credential) {
    // This has to be assigned to the oathcredential type so that we can get the accessToken property.

    const { accessToken } = credential as auth.OAuthCredential
     setAuthToken(accessToken as string)
       }
     } catch (e) {
     console.log(e)
       }
     },
     [firebase, setAuthToken]
     )

     // Method for signing up and logging in.
     const handleSignupAndLogin = React.useCallback(
     async (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
     let authError: AuthError | undefined
    try {
     event.preventDefault()
     // Try to create a new user with the email and password.
     const { user } = await firebase.auth().createUserWithEmailAndPassword(email, password)

    // If successful and we have a user the set the authToken.
     if (user) {
     const { refreshToken } = user
     setAuthToken(refreshToken)
       }
     // If there is an error set the authError to the new error
     } catch (error) {
     authError = error
     } finally {
     // If there is an authError and the code is that the email is already in use, try to sign 
    // the user in with the email and password instead.

     if (authError?.code === 'auth/email-already-in-use') {
     const { user } = await firebase.auth().signInWithEmailAndPassword(email, password)
     // We've been here before... set the authToken if there is a user.

           if (user) {
           const { refreshToken } = user
           setAuthToken(refreshToken)
           }
         }
       }
     },
     [email, password, firebase, setAuthToken]
     )

    // Effect that will reroute the user to the index.tsx file if there is an authToken
     React.useEffect(() => {
     if (authToken) {
     navigate('/')
     }
     }, [authToken])

    return (
     <form style={{ display: 'flex', flexDirection: 'column' }}>
       <FormControl>
         <InputLabel htmlFor="email">Email address</InputLabel>
         <Input id="email" aria-describedby="email-helper" value={email}
          onChange={(event) => setEmail(event.currentTarget.value)} />
         <FormHelperText id="email-helper">We&apos;ll never share your email.</FormHelperText>
       </FormControl>
       <FormControl>
         <InputLabel htmlFor="password">Password</InputLabel>
          <Input id="password" value={password} onChange={(event) =>   setPassword(event.currentTarget.value)} />
       </FormControl>
       <Button type="submit" variant="contained" color="primary" style={{ marginTop: '10px' }} onClick={handleSignupAndLogin}>
     Login / Sign Up
       </Button>
       <Button type="button" variant="contained" color="primary"
        style={{ marginTop: '10px' }} onClick={handleGoogleAuth}>
       <FontAwesomeIcon icon={faGoogle} style={{ marginRight: '10px' }} />
     Login With Google
     </Button>
     </form>
     )
    }

    export default Login

Enter fullscreen mode Exit fullscreen mode

The login component will handle sign in and sign up as well as google authentication, neat! Before all of this will work though you'll have to enable these sign in options from the firebase project console; there's a brief explanation of how to do this in the firebase documentation.

https://firebase.google.com/docs/auth/web/password-auth
https://firebase.google.com/docs/auth/web/google-signin

Once that's done you'll have to create the page that will use the login component we just created go into the pages folder and create a login.tsx file.

// login.tsx

import * as React from 'react'
import Page from '../components/Page'
import Container from '../components/Container'
import IndexLayout from '../layouts'
import Login from '../components/login'

const LoginPage = () => (
 <IndexLayout>
 <Page>
 <Container>
 <Login />
 </Container>
 </Page>
 </IndexLayout>
)

export default LoginPage
Enter fullscreen mode Exit fullscreen mode

Preventing users from seeing things they shouldn't

Now, in components create a PrivateRoute.tsx file that we will use to prevent unauthenticated users from seeing content that they shouldn't be allowed to access.

// PrivateRoute.tsx

import * as React from 'react'
import { FirebaseContext } from '../services'
import { navigate } from 'gatsby'

interface PrivateRouteProps {
 path: string
}

const PrivateRoute: React.FC<PrivateRouteProps> = ({ children, path }) => {
 const { authToken } = React.useContext(FirebaseContext)

if (!authToken && window.location.href !== path) {
 navigate(path)
 return null
 }

return <>{children}</>
}

export default PrivateRoute
Enter fullscreen mode Exit fullscreen mode

This will re-route users to the login page if they try to access anything that is nested in this component. Finally, we just have to add this component to our index.tsx file in pages.

// index.tsx

import * as React from 'react'
import { Link } from 'gatsby'
import Page from '../components/Page'
import Container from '../components/Container'
import IndexLayout from '../layouts'
import { PrivateRoute } from '../components/PrivateRoute'

const IndexPage = () => (
 <IndexLayout>
   <Page>
    <PrivateRoute path="/login">
     <Container>
      <h1>Hi people</h1>
      <p>Welcome to your new Gatsby site.</p>
      <p>Now go build something great.</p>
      <Link to="/page-2/">Go to page 2</Link>
     </Container>
    </PrivateRoute>
   </Page>
 </IndexLayout>
)

export default IndexPage
Enter fullscreen mode Exit fullscreen mode

This will tell the index page to re route to the login page if there isn't an authToken and therefore the user is not logged in. You'll still have to implement signing out but all that involves is making the token expire somehow. You'll probably want to separate the signin / signup flow at some point as well but this will get you a good start on authentication.

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!

Please follow me on Twitter: @SquashBugler

Top comments (0)