DEV Community

Cover image for Google Authentication in a Chrome Extension with Firebase
lvn1
lvn1

Posted on • Updated on

Google Authentication in a Chrome Extension with Firebase

We're writing this guide because the official one by Google is missing a few important steps, linked below:

Official Google guide: Authenticate with Firebase in a Chrome extension

Our guide: Dev.to

You can clone the repo here or start from scratch by following the tutorial below.

Github repo

If you find this guide or repo helpful, please star it:)

Prerequisites

This will work on any operating system. For the purposes of this guide we'll be using Mac OS

  • Google Chrome browser
  • A Google account (Will be linked to Firebase and Chrome Web Store)
  • A Chrome web store developer account ($5 one time fee)
  • Node.js and npm installed (Latest or LTS version)

Step 1: Create the Project Structure

a) Create a new directory for your project:

mkdir firebase-chrome-auth
cd firebase-chrome-auth
Enter fullscreen mode Exit fullscreen mode

b) Create two subdirectories:

mkdir chrome-extension
mkdir firebase-project
Enter fullscreen mode Exit fullscreen mode

Step 2: Set up the Firebase Project

a) Go to the Firebase Console.
b) Click "Add project" and follow the steps to create a new project.
c) Once created, click on "Web" to add a web app to your project.
d) Register your app with a nickname (e.g., "Chrome Extension Auth"). Also select hosting, we'll add it later in command line.
e) Copy the Firebase configuration object. You'll need this later.

const firebaseConfig = {
  apiKey: "example",
  authDomain: "example.firebaseapp.com",
  projectId: "example",
  storageBucket: "example",
  messagingSenderId: "example",
  appId: "example"
};
Enter fullscreen mode Exit fullscreen mode

f) Navigate to the firebase-project directory cd firebase-project
g) Initialize a new npm project npm init -y
h) Create a public dir mkdir public
i) Create an index.html file
firebase-project/public/index.html

<!DOCTYPE html>
<html>
 <head>
  <title>Firebase Auth for Chrome Extension</title>
 </head>
 <body>
  <h1>Firebase Auth for Chrome Extension</h1>
  <script type="module" src="signInWithPopup.js"></script>
 </body>
</html>
Enter fullscreen mode Exit fullscreen mode

j) Create a signInWithPopup.js file
firebase-project/public/signInWithPopup.js

import { initializeApp } from 'firebase/app';
import { getAuth, signInWithPopup, GoogleAuthProvider } from 'firebase/auth';

const firebaseConfig = {
  // Your web app's Firebase configuration
  // Replace with the config you copied from Firebase Console
};

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

// This gives you a reference to the parent frame, i.e. the offscreen document.
const PARENT_FRAME = document.location.ancestorOrigins[0];

const PROVIDER = new GoogleAuthProvider();

function sendResponse(result) {
  window.parent.postMessage(JSON.stringify(result), PARENT_FRAME);
}

window.addEventListener('message', function({data}) {
  if (data.initAuth) {
    signInWithPopup(auth, PROVIDER)
      .then(sendResponse)
      .catch(sendResponse);
  }
});
Enter fullscreen mode Exit fullscreen mode

k) Deploy the Firebase project

npm install -g firebase-tools (You might need to run this as sudo)
firebase login
firebase init hosting (Use existing project which we created in the firebase console earlier)

What do you want to use as your public directory? public
Configure as a single-page app (rewrite all urls to /index.html)? Yes
Set up automatic builds and deploys with GitHub? No
File public/index.html already exists. Overwrite? No

firebase deploy
Enter fullscreen mode Exit fullscreen mode

Note the hosting URL provided after deployment. You'll need this for the Chrome extension.

Step 3: Set up the Chrome Extension

a) Navigate to the chrome-extension directory

cd ../chrome-extension
npm init -y
mkdir src
mkdir src/public
mkdir src/background
mkdir src/popup
Enter fullscreen mode Exit fullscreen mode

b) We'll be using webpack to build our extension, you can install it with the following command along with firebase
npm i -D webpack webpack-cli html-webpack-plugin copy-webpack-plugin firebase and create a basic webpack config file
chrome-extension/webpack.config.js

const path = require('path'),
 CopyWebpackPlugin = require('copy-webpack-plugin'),
 HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: {
    background: './src/background/background.js',
    popup: './src/popup/popup.js',
  },
  mode: 'development',
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist'),
    clean: true,
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.join(__dirname, "src", "popup", "popup.html"),
      filename: "popup.html",
      chunks: ["firebase_config"]
    }),
    new CopyWebpackPlugin({
      patterns: [
        { from: './public/' }
      ],
    }),
  ],
};
Enter fullscreen mode Exit fullscreen mode

In the package.json file, add the script:

"scripts": {
    "release": "webpack --config webpack.config.js && zip -r dist.zip dist/*"
  },
Enter fullscreen mode Exit fullscreen mode

c) Create a manifest.json file
chrome-extension/src/public/manifest.json

{
  "manifest_version": 3,
  "name": "Firebase Auth Extension",
  "version": "1.0",
  "description": "Chrome extension with Firebase Authentication",
  "permissions": [
    "identity",
    "storage",
    "offscreen"
  ],
  "host_permissions": [
    "https://*.firebaseapp.com/*"
  ],
  "background": {
    "service_worker": "background.js",
    "type": "module"
  },
  "action": {
    "default_popup": "popup.html"
  },
  "web_accessible_resources": [
    {
      "resources": ["offscreen.html"],
      "matches": ["<all_urls>"]
    }
  ],
  "oauth2": {
    "client_id": "YOUR-GOOGLE-OAUTH-CLIENT-ID.apps.googleusercontent.com",
    "scopes": [
      "openid", 
      "email", 
      "profile"
    ]
  },
  "key": "-----BEGIN PUBLIC KEY-----\nYOURPUBLICKEY\n-----END PUBLIC KEY-----"
}
Enter fullscreen mode Exit fullscreen mode

d) Create a popup.html file
chrome-extension/src/popup/popup.html

<!DOCTYPE html>
<html>
<head>
    <title>Firebase Auth Extension</title>
</head>
<body>
    <h1>Firebase Auth Extension</h1>
    <div id="userInfo"></div>
    <button id="signInButton">Sign In</button>
    <button id="signOutButton" style="display:none;">Sign Out</button>
    <script src="popup.js"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

e) Create a popup.js file
chrome-extension/src/popup/popup.js

document.addEventListener('DOMContentLoaded', function() {
    const signInButton = document.getElementById('signInButton');
    const signOutButton = document.getElementById('signOutButton');
    const userInfo = document.getElementById('userInfo');

    function updateUI(user) {
        if (user) {
            userInfo.textContent = `Signed in as: ${user.email}`;
            signInButton.style.display = 'none';
            signOutButton.style.display = 'block';
        } else {
            userInfo.textContent = 'Not signed in';
            signInButton.style.display = 'block';
            signOutButton.style.display = 'none';
        }
    }

    chrome.storage.local.get(['user'], function(result) {
        updateUI(result.user);
    });

    signInButton.addEventListener('click', function() {
        chrome.runtime.sendMessage({action: 'signIn'}, function(response) {
            if (response.user) {
                updateUI(response.user);
            }
        });
    });

    signOutButton.addEventListener('click', function() {
        chrome.runtime.sendMessage({action: 'signOut'}, function() {
            updateUI(null);
        });
    });
});
Enter fullscreen mode Exit fullscreen mode

f) Create a background.js file
chrome-extension/src/background/background.js

const OFFSCREEN_DOCUMENT_PATH = 'offscreen.html';
const FIREBASE_HOSTING_URL = 'https://your-project-id.web.app'; // Replace with your Firebase hosting URL

let creatingOffscreenDocument;

async function hasOffscreenDocument() {
    const matchedClients = await clients.matchAll();
    return matchedClients.some((client) => client.url.endsWith(OFFSCREEN_DOCUMENT_PATH));
}

async function setupOffscreenDocument() {
    if (await hasOffscreenDocument()) return;

    if (creatingOffscreenDocument) {
        await creatingOffscreenDocument;
    } else {
        creatingOffscreenDocument = chrome.offscreen.createDocument({
            url: OFFSCREEN_DOCUMENT_PATH,
            reasons: [chrome.offscreen.Reason.DOM_SCRAPING],
            justification: 'Firebase Authentication'
        });
        await creatingOffscreenDocument;
        creatingOffscreenDocument = null;
    }
}

async function getAuthFromOffscreen() {
    await setupOffscreenDocument();
    return new Promise((resolve, reject) => {
        chrome.runtime.sendMessage({action: 'getAuth', target: 'offscreen'}, (response) => {
            if (chrome.runtime.lastError) {
                reject(chrome.runtime.lastError);
            } else {
                resolve(response);
            }
        });
    });
}

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
    if (message.action === 'signIn') {
        getAuthFromOffscreen()
            .then(user => {
                chrome.storage.local.set({user: user}, () => {
                    sendResponse({user: user});
                });
            })
            .catch(error => {
                console.error('Authentication error:', error);
                sendResponse({error: error.message});
            });
        return true; // Indicates we will send a response asynchronously
    } else if (message.action === 'signOut') {
        chrome.storage.local.remove('user', () => {
            sendResponse();
        });
        return true;
    }
});
Enter fullscreen mode Exit fullscreen mode

g) Create an offscreen.html file
chrome-extension/src/public/offscreen.html

<!DOCTYPE html>
<html>
<head>
    <title>Offscreen Document</title>
</head>
<body>
    <script src="offscreen.js"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

h) Create an offscreen.js file
chrome-extension/src/public/offscreen.js

const FIREBASE_HOSTING_URL = 'https://your-project-id.web.app'; // Replace with your Firebase hosting URL

const iframe = document.createElement('iframe');
iframe.src = FIREBASE_HOSTING_URL;
document.body.appendChild(iframe);

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
    if (message.action === 'getAuth' && message.target === 'offscreen') {
        function handleIframeMessage({data}) {
            try {
                const parsedData = JSON.parse(data);
                window.removeEventListener('message', handleIframeMessage);
                sendResponse(parsedData.user);
            } catch (e) {
                console.error('Error parsing iframe message:', e);
            }
        }

        window.addEventListener('message', handleIframeMessage);
        iframe.contentWindow.postMessage({initAuth: true}, FIREBASE_HOSTING_URL);
        return true; // Indicates we will send a response asynchronously
    }
});
Enter fullscreen mode Exit fullscreen mode

Step 4: Create an oauth ID in Google console

a) Open the Google Cloud console It should open on the Credentials page.
b) Click Create credentials > OAuth client ID.
c) Select the Chrome extension application type.
d) Name your OAuth 2.0 client and click Create. You may have to verify your app ownership.
e) Copy your Google OAUTH Client ID into the manifest.json where it says 'YOUR-GOOGLE-OAUTH-CLIENT-ID'

Step 5: Build and Upload your extension to the Chrome Web Store

a) Sign up and pay the one time $5 fee if you want to create and publish extensions in the Chrome Web Store (If you don't do this then you won't be able to get a public key or Extension ID which are needed for this tutorial to work and you will only be able to run your chrome extensions locally)
b) Navigate to the Chrome Web Store Developer Dashboard, click on the Items menu option on the left hand side and then click the '+ New item' on the right side of the page
c) Build and zip your extension using the script we created earlier like so: npm run release

(Note: I use webpack, you can use Vite or anything else you prefer)

d) Upload your Dist.zip file to the Chrome Web Store after clicking the new item button
e) Now you'll have a draft listing, you can fill in the info and save draft or complete it later.
f) You can find your Extension ID at the top of the page on the left side and to get your Public key, click on Package in the menu on the left and then click 'View public key'
g) Copy your public key in between the -----BEGIN PUBLIC KEY----- and -----END PUBLIC KEY----- lines into your manifest where it says 'YOURPUBLICKEY' make sure it's between the two '/n' statements

Step 6: Configure Firebase Authentication

a) In the Firebase Console, go to Authentication > Sign-in method.
b) Enable Google as a sign-in provider.
c) Add your Chrome extension's ID ( Step 5 -> f) ) to the authorized domains list:
The format is: chrome-extension://YOUR_EXTENSION_ID

Step 7: Load and Test the Extension

a) Open Google Chrome and go to chrome://extensions/.
b) Enable "Developer mode" in the top right corner.
c) Click "Load unpacked" and select your chrome-extension directory.
d) Click on the extension icon in Chrome's toolbar to open the popup.
e) Click the "Sign In" button and test the authentication flow.

Troubleshooting

If you encounter CORS issues, ensure your Firebase hosting URL is correctly set in both background.js and offscreen.js.

Make sure your Chrome extension's ID is correctly added to Firebase's authorized domains.

Check the console logs in the popup, background script, and offscreen document for any error messages.

Conclusion

You now have a Chrome extension that uses Firebase Authentication with an offscreen document to handle the sign-in process. This setup allows for secure authentication without exposing sensitive Firebase configuration details directly in the extension code.

Remember to replace placeholder values (like YOUR_EXTENSION_ID, YOUR-CLIENT-ID, YOUR_PUBLIC_KEY, and your-project-id) with your actual values before publishing your extension.

If you found this guide helpful please give us a star and follow us on Dev.to for more guides
Dev.to

Top comments (6)

Collapse
 
p_extension_14e1b3bb profile image
chrome_extension

Hi and thanks for the detailed instructions. I followed instructions step by step but at the end when clicking on 'sign in' button nothing happens. And I don't get any error. Does this approach need Key and if yes is it public key? What about chrome identity API for extension?

Collapse
 
lvn1 profile image
lvn1

Hi, yes you need a public key from your extension in the Chrome web store. Sorry I will update the instructions and thank you for bringing it to my attention.

You can find your public key in the Chrome Web Store Developer Dashboard -> In the left hand menu under the Build section click Package -> Then click Public Key and copy the text in between the two '-----BEGIN PUBLIC KEY----- ' lines and paste into the manifest file where I've written 'YOURPUBLICKEY' it needs to go in between the two '/n' statements so just replace the 'YOURPUBLICKEY' text exactly.

Yes the Chrome Identity Api is needed for the oauth tokens, just make sure it's in your permissions sections of the manifest file as I've written it there. Hope that helps!

Collapse
 
p_extension_14e1b3bb profile image
chrome_extension

Thanks for clarification. I haven't uploaded my extension to chrome store yet and still playing with it locally by loading package in developer mode. Do I have to upload to Chrome web store to get the public key? I have found some tutorials how to extract private and public key by using npm and running cmd and generating public key. So do I have to upload my extension to chrome web store first?

  • Even tried with the generated public key and made sure about the Firebase config but nothing happens when I click on sign in button. Should it open a redirect pop up for Google sign in? I even activated the 'allow pop up' in Chrome but didn't help. What other troubleshooting can you recommend so I can check?
Thread Thread
 
lvn1 profile image
lvn1

Thanks again for bringing it to my attention I've just added in another crucial step and sorry I missed it earlier. Please see step 4 and create an OAuth Client ID. It might work locally after that and updating your manifest file? I haven't tried without uploading to the Chrome web store yet and yes to get the public key you will need to upload it there, even in testing mode it will give you a public key.

Also for Firebase you would need the extension ID which you get by uploading to the Chrome web store, it doesn't have to be published yet, it will work in testing mode. Please let me know if you're still having issues I'll update the guide accordingly.

Collapse
 
ppri profile image
pri

Hi @lvn1,

Thank you for the post. The login worked following your tutorial! The only concern I have is the apiKey is exposed on the hosted frontend app and is also returned with the firebase response. This seems like a security vulnerability which can be badly exploited. Am I missing something? Can you please advise on how to avoid this?

Collapse
 
lvn1 profile image
lvn1

Hi @ppri, you're welcome and glad to hear it worked! Thank you for bringing up this important security concern. It's true that the Firebase configuration, including the API key, is visible in the client-side code. However, this is actually by design and not considered a significant security risk by Firebase. In the official docs: Firebase-related APIs use API keys only to identify the Firebase project or app, not for authorization

The API key you see is a project-specific public key. It's primarily used to identify your Firebase project with Google's servers. It's not a secret and doesn't grant unauthorised access to your database or project.

There are some additional security measures which can be enabled:

Restrict API Key Usage: In the Google Cloud Console, you can restrict the API key to only work for specific websites or IP addresses.
Use App Check: For additional security, especially in extensions, consider implementing Firebase App Check to ensure that any requests to your API are actually coming from your app.

While it may seem counterintuitive, exposing the Firebase config in your client-side code is a standard practice and doesn't compromise your app's security when proper measures are in place.