firebase / friendlyeats-web

Apache License 2.0
493 stars 412 forks source link

Auth service worker doesn't store firebaseConfig options between restarts #295

Open rlw87 opened 5 months ago

rlw87 commented 5 months ago

Description If the auth service worker is restarted, it loses the firebaseConfig options that it was given at registration time and throws an exception.

Reproduction Steps

  1. Open the web page for the first time. You'll see the log "Service worker installed with Firebase config" in the console
  2. Open developer tools, and under the Applications > Service workers tab, within auth-service-worker.js click "Stop"
  3. Refresh the page

Expected The service worker starts up again and the website loads as normal

Actual You see the following error in the console and the page doesn't load

FirebaseError: Firebase: Need to provide options, when not being deployed to hosting via source. (app/no-options).

I'm new to working with service workers so I might be missing something, but it looks to me as though the service worker should be storing the firebaseConfig somewhere other than in memory, so it can be reloaded on restart of the service worker. It looks like due to the firebaseConfig variable only being populated on 'install' event, it will never get the required configuration again unless you either manually unregister it and let it re-install, or it is updated to a newer version.

https://github.com/firebase/friendlyeats-web/blob/41b0a4dbfca6c106d926fd5e65db53577f99ea75/nextjs-end/auth-service-worker.js#L6C5-L6C19

ESRuth commented 5 months ago

I used the Development Tool to "Update on reload" which fixed the refresh issue, but that isn't a setting I can have my users change.

ysaied631 commented 5 months ago

Any luck with solving this?

Shahzad6077 commented 5 months ago

Hey @ESRuth, would you able to resolve this issue? Thanks

rlw87 commented 5 months ago

I've just included the firebase config in the service worker itself, rather than having the app pass it the config when it's installed. None of the values should change so I don't see why it should be a problem.

fsiatama commented 5 months ago

I am experiencing problems with the service worker, which fails upon page reload, sometimes after a few hours, or when the page is opened in a new browser tab.

timoteioros commented 4 months ago

Having the same problem. Didn't clone the friendlyeats project, I copy pasted the auth code into my Next.js proiect and I have the same issue with the service worker. Still searching for a permanent solution.

rashid4lyf commented 4 months ago

Having the same issue here as well. Hardcoding the values stops it from crashing however, obviously this is not ideal.

KirillSkomarovskiy commented 3 months ago

I used Cache API, and it solved the issue of retaining the firebase config between runs.

import { initializeApp } from "firebase/app";
import { getAuth, getIdToken } from "firebase/auth";
import { getInstallations, getToken } from "firebase/installations";

// region variables
const CACHE_NAME = 'config-cache-v1';
/** @type {FirebaseOptions | undefined} */
let CONFIG;
// endregion
// region listeners
self.addEventListener('install', (event) => {
  // extract firebase config from query string
  const serializedFirebaseConfig = new URL(location).searchParams.get('firebaseConfig');

  if (!serializedFirebaseConfig) {
    throw new Error('Firebase Config object not found in service worker query string.');
  }

  self.skipWaiting();
  event.waitUntil(saveConfig(serializedFirebaseConfig));
});

self.addEventListener("activate", (event) => {
  event.waitUntil(clients.claim());
});

self.addEventListener("fetch", (event) => {
  const { origin } = new URL(event.request.url);

  if (origin !== self.location.origin) return;

  event.respondWith(fetchWithFirebaseHeaders(event.request));
});
// endregion
// region functions
/**
 * @return string
 * */
function getConfigUrl() {
  return `${self.location.origin}/firebase-config`;
}

/**
 * @param {string} config
 *
 * return Promise<void>
 * */
async function saveConfig(config) {
  const cache = await caches.open(CACHE_NAME);

  const response = new Response(config, {
    headers: { 'Content-Type': 'application/json' }
  });

  await cache.put(getConfigUrl(), response);
}

/**
 * @param {Request} request
 *
 * @return Response
 * */
async function fetchWithFirebaseHeaders(request) {
  const config = await getConfig();

  if (!config) {
    return await fetch(request);
  }

  const app = initializeApp(config);
  const auth = getAuth(app);
  const installations = getInstallations(app);
  const headers = new Headers(request.headers);
  const [authIdToken, installationToken] = await Promise.all([
    getAuthIdToken(auth),
    getToken(installations),
  ]);
  headers.append("Firebase-Instance-ID-Token", installationToken);

  if (authIdToken) headers.append("Authorization", `Bearer ${authIdToken}`);

  const newRequest = new Request(request, { headers });

  return await fetch(newRequest);
}

/**
 * @param {Auth} auth
 *
 * @return Promise<string | undefined>
 * */
async function getAuthIdToken(auth) {
  await auth.authStateReady();

  if (!auth.currentUser) return;

  return await getIdToken(auth.currentUser);
}

/**
 * @return FirebaseOptions | undefined
 * */
async function getConfig() {
  if (CONFIG) return CONFIG;

  const cache = await caches.open(CACHE_NAME);
  const configResponse = await cache.match(getConfigUrl());

  if (!configResponse) {
    return;
  }

  const config = await configResponse.json();
  CONFIG = config;

  return CONFIG;
}
// endregion
heckchuckman commented 3 months ago

Hi @KirillSkomarovskiy - thanks for this. I'm still getting the error:

FirebaseError: Firebase: Need to provide options, when not being deployed to hosting via source. (app/no-options).

@rlw87 can you add a code snippet of your approach?

Thanks all!

piotrsliwka333 commented 3 months ago

Same problem from my side, just by coping codelab-friendlyeats-web and deploying it.

leandroz commented 3 months ago

Same here, it is crazy how hard it is to find a working example.

pashpashpash commented 3 months ago

Same problem here!

pashpashpash commented 3 months ago

Same problem from my side, just by coping codelab-friendlyeats-web and deploying it.

Not inspiring much confidence in using firebase app hosting for nextjs...

pashpashpash commented 3 months ago
import { initializeApp } from "firebase/app";
import { getAuth, getIdToken } from "firebase/auth";
import { getInstallations, getToken } from "firebase/installations";

// old code (dont include this
// // this is set during install
// let firebaseConfig;

// self.addEventListener('install', event => {
//   // extract firebase config from query string
//   const serializedFirebaseConfig = new URL(location).searchParams.get('firebaseConfig');

//   if (!serializedFirebaseConfig) {
//     throw new Error('Firebase Config object not found in service worker query string.');
//   }

//   firebaseConfig = JSON.parse(serializedFirebaseConfig);
//   console.log("Service worker installed with Firebase config", firebaseConfig);
// });

// Default hardcoded Firebase configuration -- put your config here

let firebaseConfig = {
  apiKey: "xxxxxxxxxxxxxxxxxx",
  authDomain: "xxxxxxxxxxxxxxxxx",
  projectId: "xxxxxxxxxxxxxxxxxxx",
  storageBucket: "xxxxxxxxxxxxxxxxxxx",
  messagingSenderId: "xxxxxxxxxxxxxxxxxxxxxxx",
  appId: "xxxxxxxxxxxxxxxxxxxxxxx",
  measurementId: "xxxxxxxxxxxxxxxxxxxxxxxxxxx"
};

// Handle the 'install' event and extract the firebaseConfig from the query string if present
self.addEventListener('install', event => {
  const serializedFirebaseConfig = new URL(location).searchParams.get('firebaseConfig');

  if (serializedFirebaseConfig) {
    try {
      firebaseConfig = JSON.parse(serializedFirebaseConfig);
      console.log("Service worker installed with Firebase config from query string", firebaseConfig);
    } catch (error) {
      console.error("Failed to parse Firebase config from query string", error);
    }
  } else {
    console.log("Service worker installed with hardcoded Firebase config", firebaseConfig);
  }
});

self.addEventListener("fetch", (event) => {
  console.log("Fetching with Firebase config:", firebaseConfig);
  const { origin } = new URL(event.request.url);
  if (origin !== self.location.origin) return;
  event.respondWith(fetchWithFirebaseHeaders(event.request));
});

async function fetchWithFirebaseHeaders(request) {
  const app = initializeApp(firebaseConfig);
  const auth = getAuth(app);
  const installations = getInstallations(app);
  const headers = new Headers(request.headers);
  const [authIdToken, installationToken] = await Promise.all([
    getAuthIdToken(auth),
    getToken(installations),
  ]);
  headers.append("Firebase-Instance-ID-Token", installationToken);
  if (authIdToken) headers.append("Authorization", `Bearer ${authIdToken}`);
  const newRequest = new Request(request, { headers });
  return await fetch(newRequest);
}

async function getAuthIdToken(auth) {
  await auth.authStateReady();
  if (!auth.currentUser) return;
  return await getIdToken(auth.currentUser);
}

Then run

npx esbuild auth-service-worker.js --bundle --outfile=public/auth-service-worker.js

to compile the new service worker file and put it in public so that it works locally. For prod, you don't need to worry about this step, it will auto run this as part of the build process.

adrolc commented 3 months ago

The problem is that the configuration is initialized during the install event. When the service worker is stoped and then resumed, the install event will not be triggered again, leaving firebaseConfig undefined. You need to check in the fetch event whether firebaseConfig is set, if not, extract the config from the query string again:

self.addEventListener("fetch", (event) => {
  if (!firebaseConfig) {
    const serializedFirebaseConfig = new URL(location).searchParams.get(
      "firebaseConfig"
    );
    firebaseConfig = JSON.parse(serializedFirebaseConfig);
  }
  // rest of code
});

But I think you could get rid of the install event and just initialize it globally:

const serializedFirebaseConfig = new URL(location).searchParams.get(
  "firebaseConfig"
);

if (!serializedFirebaseConfig) {
  throw new Error(
    "Firebase Config object not found in service worker query string."
  );
}

const firebaseConfig = JSON.parse(serializedFirebaseConfig);

self.addEventListener("fetch", (event) => {
  const { origin } = new URL(event.request.url);
  if (origin !== self.location.origin) return;
  event.respondWith(fetchWithFirebaseHeaders(event.request));
});
//...
chrisstayte commented 2 months ago

Did this work for you @adrolc I am having the same issues right now as well.

alex0916 commented 2 months ago

The problem is that the configuration is initialized during the install event. When the service worker is stoped and then resumed, the install event will not be triggered again, leaving firebaseConfig undefined. You need to check in the fetch event whether firebaseConfig is set, if not, extract the config from the query string again:

self.addEventListener("fetch", (event) => {
  if (!firebaseConfig) {
    const serializedFirebaseConfig = new URL(location).searchParams.get(
      "firebaseConfig"
    );
    firebaseConfig = JSON.parse(serializedFirebaseConfig);
  }
  // rest of code
});

But I think you could get rid of the install event and just initialize it globally:

const serializedFirebaseConfig = new URL(location).searchParams.get(
  "firebaseConfig"
);

if (!serializedFirebaseConfig) {
  throw new Error(
    "Firebase Config object not found in service worker query string."
  );
}

const firebaseConfig = JSON.parse(serializedFirebaseConfig);

self.addEventListener("fetch", (event) => {
  const { origin } = new URL(event.request.url);
  if (origin !== self.location.origin) return;
  event.respondWith(fetchWithFirebaseHeaders(event.request));
});
//...

This worked for me 👍🏽

RonakDoshiTMI commented 2 months ago

+1