publication logo
post cover

Firebase Authentication with React in an MV3 Chrome Extension

Diving deep into everything you need to make Firebase Auth work with your MV3 Chrome Extension

author profile

Stefan Aleksic

May 20 2022

11 mins read

Manifest V3 added several changes to both security policies as well as infrastructure. This can be confusing as different tools have been slow to update their documentation and still reference Manifest V2, but with the help of this post, navigating these changes should be a breeze!


To see a completed version of the code referenced here, check out with-firebase.


Plasmo Framework

We're going to be using the Plasmo Framework for this blog post. It'll make it super easy for us to get started and build a modern Chrome extension. Plasmo offers a browser extension development suite that makes writing modern Chrome extensions much better with live-reloading, Tailwind support, .env file parsing, and more.


Pre-requisites

  • NPM Installed
  • Package Manager (npm, yarn, pnpm, etc.)

Quickstart

Running pnpm dlx plasmo init will give us a scaffolded project that we can immediately jump into.


pnpm dlx plasmo init


Before starting to code, we need to hook up all the plumbing with the Google Cloud Console and Firebase Console.


The high-level steps look like this: (Don't worry if it seems like a lot, we'll go through them in detail!)

  • Create a Google Cloud Project
  • Create an OAuth Consent Page
  • Create an OAuth Client that uses that consent page
  • Store the client ID within your manifest.json
  • Create a new Firebase project that links to your newly created Google Cloud Project
  • Create a new Firebase app within that project
  • Store your app's config
  • Enable Authentication
  • Enable the Google Sign-in Provider
  • Write code to do cool stuff!

Whew! Let's get started!


Step 1: Create a Google Cloud Project

Go to your Google Cloud Console and create a new project.

Go to your project picker.

Go to your project picker



Click on New Project.

Click on New Project

Name it whatever you want, and click Create.

Name it whatever you want, and click Create


Click on Select Project to go to your project.

Click on Select Project to go to your project

Step 2: Create an OAuth Consent Page

Go to the APIs & Services section of your project. This section is where we will configure our OAuth Consent Page and create our OAuth Client!

Go to the APIs & Services section of your project


We'll need to create a consent screen. It's what users see when they're asked whether they're okay with giving your app-specific information.

Here's an example of what a consent screen looks like:

Sneak peek of what a consent screen looks like

We need to go to the OAuth consent screen page to create one of these.

Go to OAuth Consent Screen

Select your OAuth User Type. For us, we'll be using External.

Select your OAuth User Type


Add metadata about your consent screen.

Add metadata about your consent screen

Set your developer contact info.

Set your developer contact info


Set your scopes.

Note: For Firebase projects, this isn't necessary as Firebase will set them up for you.

Set your scopes

Add your test users.

Note: These will be the email addresses allowed to use the OAuth consent screen until Google reviews your app and approves it for broader use.

Add your test users

Great! Now that we've added our consent screen, it's time to create our OAuth Client!

Step 3: Create an OAuth Client that uses that consent page

Let's go to the Credentials section.

Going to the Credentials section


Click on Create Credentials and then OAuth Client ID. We want to create a new Client ID using that consent page we just made!

Creating Credentials

Here, we want to specify that the Client will be a Chrome app. Google has stated that they are planning to deprecate Chrome apps but don't worry, this use of Chrome App is just an artifact. It works for Chrome Extensions as well.

Setting Client to be a Chrome App



Now we need to specify the Application ID. This is a unique ID that every Chrome extension has. For example, LastPass's ID is hdokiejnpimakedhajhdlcegeplioahd.

Side Quest: Demystifying Chrome Extension IDs:

A Chrome Extension ID is "the first 128 bits of the SHA256 of an RSA public key encoded in base 16" (according to this Stackoverflow post)


Sounds complicated, but it's no big deal once we break it down!


RSA key pairs back all Chrome IDs. We'll need to generate an RSA private key and use it for our local Chrome Extension version.


To generate a private key, we'll use OpenSSL:

openssl genrsa 2048

This gives us a private key in the traditional key format, but we'll want to convert this to PKCS#8 format. I won't bore you with the details. You can read more about it here.


To convert it to the correct format and store it into a file, we'll want to do the following:

openssl genrsa 2048 | openssl pkcs8 -topk8 -nocrypt -out /tmp/key.pem


This creates a private key file in our /tmp directory. Make sure to put this private key file somewhere safe. You'll be using this for the duration of your local development with this Chrome extension!


To associate this private key with our Chrome extension, we'll need to tell Chrome about it.


Luckily, we can specify this with a key in our package.json's manifest. Because we're using Plasmo, we don't have to tinker with a manifest.json. It gets built for us. But whenever we want to add a custom key to it, we can use the manifest field within our package's package.json!


In this case, the key we want to add is called. Well... key! And it's expecting a raw representation of the public key encoded in base64 with no line breaks.


openssl rsa -in key.pem -pubout -outform DER | openssl base64 -A


Now we can take that value and put it in our config inside our package.json:

Adding the key to the manifest via package.json

Now we can derive our Chrome Extension's ID based on this key value! We do this by getting the SHA256 hash, only taking the first 32 characters, and then replacing all 0-9a-f with a-p. Whew. Let's see what that looks like:


openssl rsa -in key.pem -pubout -outform DER | shasum -a 256 | head -c32 | tr 0-9a-f a-p


Side note, thanks to Rob Wu, from the Mozilla Add-ons team for the helpful commands!

Now that we have the ID and it will be constant for our local development version, we can add it as the Application ID:

Note: You will need to re-do these steps and create a new Client ID when you know what your production Chrome Extension ID is (i.e., the version that gets uploaded to the Chrome Web Store)

Adding your Application ID


With your client now created, you can copy that Client ID!

Getting your client ID

Step 4: Store the client ID

Since the Plasmo Framework abstracts away your manifest.json, we have another way of adding fields into the manifest manually. We use the package.json to do this, using a manifest key!


Go to your package.json and add the following within the manifest field:


"oauth2": {
 "client_id": "your_client_id",
     "scopes": [
       "https://www.googleapis.com/auth/userinfo.email",
       "https://www.googleapis.com/auth/userinfo.profile"
     ]
}

Put the client ID you just created into the client_id field.

Step 5: Creating a New Firebase Project

We need to create a new Firebase project and link it up to our OAuth Client!

Adding a new Firebase Project

Now link your new Google Cloud project!

Link your Google Cloud project


We need to create a new app within our new Firebase project. Click on the web button to create a new app.

Click the web button to create a new project


Give your Firebase app a cool new name.

Create a new Firebase app

We'll get a code snippet that we can use in our code! Put this down somewhere for the time being. We'll reference it later!

Firebase Config code snippet


Install firebase

Make sure to run pnpm install firebase in order to add firebase to your project's dependencies! ‎


Step 6: Store your app's config

Create a firebase.ts file in your package and copy the Firebase code snippet there.


import { initializeApp } from "firebase/app"
import { getAuth } from "firebase/auth"

const firebaseConfig = {
  apiKey: "aBunChOfRaNdOmCharaCTErsHeRE",
  authDomain: "your-projects-name.firebaseapp.com",
  projectId: "your-projects-name-1234",
  storageBucket: "your-projects-name-1234.appspot.com",
  messagingSenderId: "123456789",
  appId: "1:10203020:web:somethingHere1000"
}

export const app = initializeApp(firebaseConfig)
export const auth = getAuth(app)


To make it easier to work with, we can also create an auth object with getAuth.


Step 7: Set up Authentication within Firebase

Going to the Authentication Section

Step 8: Enable Authentication

If it's your first time going to this section, you'll need to click "Get started"

Click get started in Firebase Authentication

Step 9: Enable the Google Sign-in Provider

Go to the Sign-in method tab, and click on Google.

Adding the Google Provider

Then we need to hit enable, and we're good to go.

Enabling the Google Sign-in provider

Step 10: Write Code and Profit

Now that we have everything in place, let's write some code!


For the best user experience, open a terminal window and run the dev command. This will enable features like live-reloading, so you don't have to refresh your extension when you make changes.


pnpm run dev


Note: if you're modifying the manifest via the package.json, you must manually reload the extension in Chrome. You must manually reload any manifest changes.


In popup.tsx, we can write something like this:

import {
  GoogleAuthProvider,
  User,
  browserLocalPersistence,
  onAuthStateChanged,
  setPersistence,
  signInWithCredential
} from "firebase/auth"

import { useEffect, useState } from "react"

// This is the firebase.ts file we created a few
// steps ago when we received our config!
import { auth } from "./firebase"

// We'll need to specify that we want Firebase to store
// our credentials in localStorage rather than in-memory
setPersistence(auth, browserLocalPersistence)


With everything set up, let's see how we can leverage Firebase within our React app.


function IndexPopup() {
  const [isLoading, setIsLoading] = useState(false)
  const [user, setUser] = useState<User>(null)

  // Whenever the user clicks logout, we need to 
  // use the auth object we imported from our firebase.ts
  // file and sign them out!
  const onLogoutClicked = async () => {
    if (user) {
      await auth.signOut()
    }
  }

  // When the user clicks log in, we need to ask Chrome
  // to log them in, get their Google auth token, 
  // send it to Firebase, and let Firebase do its magic
  // if everything worked, we'll get a user object from them
  const onLoginClicked = () => {
    chrome.identity.getAuthToken({ interactive: true }, async function (token) {
      if (chrome.runtime.lastError || !token) {
        console.error(chrome.runtime.lastError)
        setIsLoading(false)
        return
      }
      if (token) {
        const credential = GoogleAuthProvider.credential(null, token)
        try {
          // There's no need to do anything with what this returns
          // since we're keeping track of the user object with
          // onAuthStateChanged
          await signInWithCredential(auth, credential)
        } catch (e) {
          console.error("Could not log in. ", e)
        }
      }
    })
  }

  // We register this listener once when this component starts
  useEffect(() => {
    // Whenever the auth changes, we make sure we're no longer loading
    // and set the user! On login, this will populate with a new user
    // on logout, this will make user null
    onAuthStateChanged(auth, (user) => {
      setIsLoading(false)
      setUser(user)
    })
  }, [])

  return (
    <div
      style={{
        display: "flex",
        flexDirection: "column",
        padding: 16
      }}>
      <h1>
        Welcome to your <a href="https://www.plasmo.com">Plasmo</a> Extension!
      </h1>
      {!user ? (
        <button
          onClick={() => {
            setIsLoading(true)
            onLoginClicked()
          }}>
          Log in
        </button>
      ) : (
        <button
          onClick={() => {
            setIsLoading(true)
            onLogoutClicked()
          }}>
          Log out
        </button>
      )}
      <div>
        {isLoading ? "Loading..." : ""}
        {!!user ? (
          <div>
            Welcome to Plasmo, {user.displayName} your email address is{" "}
            {user.email}
          </div>
        ) : (
          ""
        )}
      </div>
    </div>
  )
}

export default IndexPopup

And that's it! No need to manually do anything! The Plasmo framework will automatically build this, and you'll see your popup in action.


When a user's logged out, they see the following:


Logged out

‎When they click on log in, Chrome will direct them to log in with Google.


Consent Screen

‎This will use the consent screen we created a few steps ago! So if you want to modify it, you'll have to go to your Google Cloud OAuth Consent Screen section and make any modifications there.


Once the user is logged in, we can show them some info about them!


Image


Common Pitfalls

As there are many moving parts, things can go wrong. You might see these issues when you're working through this integration process. Here are some of the most common ones I've seen:


access_token audience is not for this project

  • This means the OAuth Client ID you have in your manifest.json (package.json for Plasmo Framework folks) is for a Google project that isn't linked with your Firebase project. So you must've copied the wrong Client ID. Check which Google project you connected Firebase to, and use its Client ID, instead.
  • If you tried the above, and you're sure that the client ID matches, it might be an issue with cached auth tokens. Go to chrome://identity-internals and revoke the cached token. We will add this to the Firebase example with identity.removeCachedAuthToken soon.


Conclusion

Congrats for making it all the way through! Adding Firebase Auth for a Chrome Extension has many moving parts, but if you follow the steps carefully and use the Plasmo Framework, you will have a fully working Firebase Auth project using React in no time!

Read more posts like this in your inbox

Subscribe to the newsletter

chrome extensionfirebasemv3