20

I am trying to set-up a Firebase-messaging-sw.js file (for web push notifications). I am wondering if there is a way to avoid exposing my Firebase config data to the public as much as possible - though it might be revealed anyways? (I'm not too sure about the nuances)

I've tried following: How can I customize my Service Worker based on environment variables? But the answer's swEnvbuild doesn't seem to be running, as the swenv.js file is not found. I suspect it might need to be set-up differently in React?

(first question, please feel free to provide constructive criticisms of my question)

Alexander Kim
  • 301
  • 2
  • 4

5 Answers5

12

I recently had to do this with a CRA app, it's not easy to find information on it so I figured I should share my solution. Assuming you've already changed serviceWorker.unregister() to serviceWorker.register() in ./src/index.js, and have a .env file with your variables set in the root of your project, then you can update ./src/serviceWorker.js to include your process.env variables as a query string.

In the register function in serviceWorker.js, update const swUrl as shown below, notice the const firebaseConfig w/process.env, declared before swUrl..
./src/serviceWorker.js:

// Convert environment variables to URL `search` parameters
const firebaseConfig = new URLSearchParams({
  apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
  authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
  databaseURL: process.env.REACT_APP_FIREBASE_DATABASE_URL,
  projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
  storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.REACT_APP_FIREBASE_APP_ID,
  measurementId: process.env.REACT_APP_FIREBASE_MEASUREMENT_ID
}).toString();

// Service worker URL w/config variables
const swUrl = `${process.env.PUBLIC_URL}/firebase-messaging-sw.js?${firebaseConfig}`;

then in ./public/firebase-messaging-sw.js (create it if it doesn't exist), you can do something like the following..
./public/firebase-messaging-sw.js

importScripts('https://www.gstatic.com/firebasejs/8.0.2/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/8.0.2/firebase-messaging.js');

// Set Firebase configuration, once available
self.addEventListener('fetch', () => {
  const urlParams = new URLSearchParams(location.search);
  self.firebaseConfig = Object.fromEntries(urlParams);
});

// "Default" Firebase configuration (prevents errors)
const defaultConfig = {
  apiKey: true,
  projectId: true,
  messagingSenderId: true,
  appId: true,
};

// Initialize Firebase app
firebase.initializeApp(self.firebaseConfig || defaultConfig);
const messaging = firebase.messaging();

// Configure message handler (assumes backend is set up)
messaging.onBackgroundMessage((payload) => {
  const { icon, body, title } = payload.data;
  self.registration.showNotification(title, { body, icon });
});    

If there's a more ideal solution, would love to hear about it, but this configuration worked for me.

iPzard
  • 2,018
  • 2
  • 14
  • 24
11

I found this article which uses cra-append-sw to append the env vars. Then I created two pre scripts in my package.json. When I run npm start the prestart script runs creating a [root folder]/public/firebase-messaging-sw.js file that contains the env vars (after being processed by webpack).

Implementation

I created a [root folder]/firebase-messaging-sw.js. This file will be processed by webpack replacing the values of the env vars.

// Give the service worker access to Firebase Messaging.
// Note that you can only use Firebase Messaging here. Other Firebase libraries
// are not available in the service worker.
importScripts('https://www.gstatic.com/firebasejs/8.1.1/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/8.1.1/firebase-messaging.js');

// Initialize the Firebase app in the service worker by passing in
// your app's Firebase config object.
// https://firebase.google.com/docs/web/setup#config-object
firebase.initializeApp({
    apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
    authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
    databaseURL: process.env.REACT_APP_FIREBASE_DATABASE_URL,
    projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
    storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
    messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
    appId: process.env.REACT_APP_FIREBASE_APP_ID,
    measurementId: process.env.REACT_APP_FIREBASE_MEASUREMENT_ID,
});

// Retrieve an instance of Firebase Messaging so that it can handle background
// messages.
const messaging = firebase.messaging();
messaging.onBackgroundMessage(function (payload) {
    console.log('[firebase-messaging-sw.js] Received background message ', payload);
    // Customize notification here
    const notificationTitle = 'Background Message Title';
    const notificationOptions = {
        body: 'Background Message body.',
        icon: '/logo.png'
    };

    self.registration.showNotification(notificationTitle, notificationOptions);
});

then I have [root folder]/.env.dev and [root folder]/.env.prod

REACT_APP_FIREBASE_API_KEY=A...
REACT_APP_FIREBASE_AUTH_DOMAIN=d...
REACT_APP_FIREBASE_DATABASE_URL=h...
REACT_APP_FIREBASE_PROJECT_ID=d...
REACT_APP_FIREBASE_STORAGE_BUCKET=d...
REACT_APP_FIREBASE_MESSAGING_SENDER_ID=7...
REACT_APP_FIREBASE_APP_ID=1...
REACT_APP_FIREBASE_MEASUREMENT_ID=G...

And finally, i added 2 pre scripts in my package.json

...
"scripts": {
    "prestart": "cra-append-sw --mode dev --env ./.env.dev ./firebase-messaging-sw.js",
    "prebuild": "cra-append-sw --mode build --env ./.env.prod ./firebase-messaging-sw.js",
...
Jonathan Morales Vélez
  • 3,030
  • 2
  • 30
  • 43
1

There is no way of accessing the environment variables in the service worker files as these load before environment variables are accessible when a website is loading. It is fine to have the Firebase keys displayed on the service worker file page. Refer to this answer (recommended by Google Cloud Collective) to understand it better.

Now to achieve the end goal to have 2 different Firebase service worker files for lower and production environments, I have added the following code to a middleware.ts file in my React + Typescript application codebase. You can also add this to a js file if you are using that as the main language in your application. Save this file to your src folder or pages folder.

import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/firebase-messaging-sw.js')) {
    if (request.nextUrl.hostname === 'your-website-prod-hostname.com') {
      return NextResponse.rewrite(new URL('/firebase-messaging-sw-prod.js', request.url));
    }
    return NextResponse.rewrite(new URL('/firebase-messaging-sw-dev.js', request.url));
  }
}

export const config = {
  matcher: '/firebase-messaging-sw*',
}

The config will only run the middleware for paths that start with the mentioned matcher.

LW001
  • 2,452
  • 6
  • 27
  • 36
Mane
  • 11
  • 3
0

I had real troubles with this one myself. The service worker gets involved in the stack way before your environment gets bootstrapped so it makes sense that it doesn't have access to your .Env variables.

My Solution

I built an npm module that on build, using webpack, extracts your "safe" versioning variables from your .env file and puts them into a stand-alone JS file. You can then go ahead and import this file and use it in your service worker.

https://www.npmjs.com/package/vue-enverywhere

Disclaimer:

I know this is for vue, but its webpack, and it's not vue specific. Also, You might be better to just copy the code, and not use the module. This was more of a fun exercise for myself :)

Sweet Chilly Philly
  • 3,014
  • 2
  • 27
  • 37
0

In index.js file (or wherever you want to register service worker) :

if ("serviceWorker" in navigator) {
  console.log("Registration started");
  const firebaseConfig = encodeURIComponent(
    JSON.stringify({
      apiKey: process.env.FCM_API_KEY,
      projectId: process.env.FCM_PROJECT_ID,
      messagingSenderId: process.env.FCM_SENDER_ID,
      appId: process.env.FCM_APP_ID,
    })
  );
  navigator.serviceWorker
    .register(
      `../../../firebase-messaging-sw.js?firebaseConfig=${firebaseConfig}`
    )
    .then(function (registration) {
      console.log("Registration successful, scope is:", registration.scope);
    })
    .catch(function (err) {
      console.log("Service worker registration failed, error:", err);
    });

In Service Worker, firebase-messaging-sw.js :

importScripts("https://www.gstatic.com/firebasejs/8.10.0/firebase-app.js");
importScripts(
  "https://www.gstatic.com/firebasejs/8.10.0/firebase-messaging.js"
);

firebase.initializeApp(
  JSON.parse(new URL(location).searchParams.get("firebaseConfig"))
);

firebase.messaging();

This is enough for receiving Background Push Notifications on sending notification-type message.

this solution will work, Just remember to edit the path to your Service-worer file (while registering in index.js or so), according to your project, Or It will give "Mime Error".

  • I am unable to use or display the firebaseConfig from the url params. I recieve false when logging `new URL(location).searchParams.has('firebaseConfig')`. I have tripple checked the path. – Rafael Zasas Apr 11 '22 at 05:22
  • @RafaelZasas If you are console.logging in service worker file and the output is false, that means the the file is getting registered. Also the path to the file must be correct in navigator.serviceWorker.register(..). That leaves us to other possible cases, - check if you are registering it multiple time from different places. - Make sure again the path is correct, i.e the URL formation is proper. – Ankit Gupta Apr 11 '22 at 14:31