Sitzungsverwaltung mit Service Workern

Firebase Auth ermöglicht die Verwendung von Service Workern, Firebase-ID-Tokens für die Sitzungsverwaltung. Dies bietet folgende Vorteile:

  • Möglichkeit, ohne zusätzliche Arbeit ein ID-Token an jede HTTP-Anfrage vom Server zu übergeben.
  • Die Möglichkeit, das ID-Token ohne zusätzlichen Umlauf oder Latenzen.
  • Synchronisierte Backend- und Frontend-Sitzungen Diese Lösung eignet sich für Anwendungen, die auf Firebase-Dienste wie Realtime Database, Firestore usw. und einige externe serverseitige Ressourcen (z. B. SQL-Datenbanken) zugreifen müssen. Außerdem kann der Service Worker auf dieselbe Sitzung zugreifen. Web Worker oder freigegebener Worker.
  • Es ist nicht mehr erforderlich, Firebase Auth-Quellcode auf jeder Seite einzufügen (verringert die Latenz). Der einmal geladene und initialisierte Service Worker die Sitzungsverwaltung für alle Kunden im Hintergrund.

Übersicht

Firebase Auth ist für die clientseitige Ausführung optimiert. Tokens werden gespeichert in Webspeicher. So lässt sich die Integration auch in andere Firebase-Dienste wie Realtime Database, Cloud Firestore und Cloud Storage ganz einfach realisieren. Um Sitzungen serverseitig zu verwalten, müssen ID-Tokens abgerufen und an den Server übergeben werden.

Web

import { getAuth, getIdToken } from "firebase/auth";

const auth = getAuth();
getIdToken(auth.currentUser)
  .then((idToken) => {
    // idToken can be passed back to server.
  })
  .catch((error) => {
    // Error occurred.
  });

Web

firebase.auth().currentUser.getIdToken()
  .then((idToken) => {
    // idToken can be passed back to server.
  })
  .catch((error) => {
    // Error occurred.
  });

Das bedeutet jedoch, dass ein Skript vom Client ausgeführt werden muss, um den neuestes ID-Token und übergeben es über den Anfrage-Header POST an den Server. Text usw.

Dies kann nicht skaliert werden und stattdessen sind serverseitige Sitzungscookies erforderlich. ID-Tokens können als Sitzungs-Cookies festgelegt werden. Diese sind jedoch kurzlebig und müssen vom Client aktualisiert und dann nach Ablauf als neue Cookies festgelegt werden. Dies kann einen zusätzlichen Rück- und Vorlauf erfordern, wenn der Nutzer die Website schon länger nicht besucht hat.

Während Firebase Auth cookiebasierte Lösung zur Sitzungsverwaltung, Diese Lösung eignet sich am besten für serverseitige httpOnly-Cookie-basierte Anwendungen. und ist schwieriger zu verwalten, da Client-Tokens nicht synchronisiert, insbesondere wenn Sie auch andere clientbasierte Firebase- .

Stattdessen können Sie Dienstprogramme verwenden, um Nutzersitzungen für die serverseitige Nutzung zu verwalten. Dies funktioniert aus folgenden Gründen:

  • Service Worker haben Zugriff auf den aktuellen Firebase Auth-Status. Der aktuelle Das Nutzer-ID-Token kann vom Service Worker abgerufen werden. Wenn das Token abgelaufen ist, aktualisiert das Client-SDK es und gibt ein neues zurück.
  • Service Worker können Abrufanfragen abfangen und ändern.

Service Worker-Änderungen

Der Service Worker muss die Auth-Bibliothek und die Möglichkeit zum Abrufen Das aktuelle ID-Token, wenn ein Nutzer angemeldet ist.

Web

import { initializeApp } from "firebase/app";
import { getAuth, onAuthStateChanged, getIdToken } from "firebase/auth";

// Initialize the Firebase app in the service worker script.
initializeApp(config);

/**
 * Returns a promise that resolves with an ID token if available.
 * @return {!Promise<?string>} The promise that resolves with an ID token if
 *     available. Otherwise, the promise resolves with null.
 */
const auth = getAuth();
const getIdTokenPromise = () => {
  return new Promise((resolve, reject) => {
    const unsubscribe = onAuthStateChanged(auth, (user) => {
      unsubscribe();
      if (user) {
        getIdToken(user).then((idToken) => {
          resolve(idToken);
        }, (error) => {
          resolve(null);
        });
      } else {
        resolve(null);
      }
    });
  });
};

Web

// Initialize the Firebase app in the service worker script.
firebase.initializeApp(config);

/**
 * Returns a promise that resolves with an ID token if available.
 * @return {!Promise<?string>} The promise that resolves with an ID token if
 *     available. Otherwise, the promise resolves with null.
 */
const getIdToken = () => {
  return new Promise((resolve, reject) => {
    const unsubscribe = firebase.auth().onAuthStateChanged((user) => {
      unsubscribe();
      if (user) {
        user.getIdToken().then((idToken) => {
          resolve(idToken);
        }, (error) => {
          resolve(null);
        });
      } else {
        resolve(null);
      }
    });
  });
};

Alle Abrufanfragen an den Ursprung der App werden abgefangen. Wenn ein ID-Token ist verfügbar und wird durch den Header an die Anfrage angehängt. Serverseitig, Anfrage werden die Header auf das ID-Token geprüft, verifiziert und verarbeitet. Im Service Worker-Script wird die Abrufanfrage abgefangen und geändert.

Web

const getOriginFromUrl = (url) => {
  // https://stackoverflow.com/questions/1420881/how-to-extract-base-url-from-a-string-in-javascript
  const pathArray = url.split('/');
  const protocol = pathArray[0];
  const host = pathArray[2];
  return protocol + '//' + host;
};

// Get underlying body if available. Works for text and json bodies.
const getBodyContent = (req) => {
  return Promise.resolve().then(() => {
    if (req.method !== 'GET') {
      if (req.headers.get('Content-Type').indexOf('json') !== -1) {
        return req.json()
          .then((json) => {
            return JSON.stringify(json);
          });
      } else {
        return req.text();
      }
    }
  }).catch((error) => {
    // Ignore error.
  });
};

self.addEventListener('fetch', (event) => {
  /** @type {FetchEvent} */
  const evt = event;

  const requestProcessor = (idToken) => {
    let req = evt.request;
    let processRequestPromise = Promise.resolve();
    // For same origin https requests, append idToken to header.
    if (self.location.origin == getOriginFromUrl(evt.request.url) &&
        (self.location.protocol == 'https:' ||
         self.location.hostname == 'localhost') &&
        idToken) {
      // Clone headers as request headers are immutable.
      const headers = new Headers();
      req.headers.forEach((val, key) => {
        headers.append(key, val);
      });
      // Add ID token to header.
      headers.append('Authorization', 'Bearer ' + idToken);
      processRequestPromise = getBodyContent(req).then((body) => {
        try {
          req = new Request(req.url, {
            method: req.method,
            headers: headers,
            mode: 'same-origin',
            credentials: req.credentials,
            cache: req.cache,
            redirect: req.redirect,
            referrer: req.referrer,
            body,
            // bodyUsed: req.bodyUsed,
            // context: req.context
          });
        } catch (e) {
          // This will fail for CORS requests. We just continue with the
          // fetch caching logic below and do not pass the ID token.
        }
      });
    }
    return processRequestPromise.then(() => {
      return fetch(req);
    });
  };
  // Fetch the resource after checking for the ID token.
  // This can also be integrated with existing logic to serve cached files
  // in offline mode.
  evt.respondWith(getIdTokenPromise().then(requestProcessor, requestProcessor));
});

Web

const getOriginFromUrl = (url) => {
  // https://stackoverflow.com/questions/1420881/how-to-extract-base-url-from-a-string-in-javascript
  const pathArray = url.split('/');
  const protocol = pathArray[0];
  const host = pathArray[2];
  return protocol + '//' + host;
};

// Get underlying body if available. Works for text and json bodies.
const getBodyContent = (req) => {
  return Promise.resolve().then(() => {
    if (req.method !== 'GET') {
      if (req.headers.get('Content-Type').indexOf('json') !== -1) {
        return req.json()
          .then((json) => {
            return JSON.stringify(json);
          });
      } else {
        return req.text();
      }
    }
  }).catch((error) => {
    // Ignore error.
  });
};

self.addEventListener('fetch', (event) => {
  /** @type {FetchEvent} */
  const evt = event;

  const requestProcessor = (idToken) => {
    let req = evt.request;
    let processRequestPromise = Promise.resolve();
    // For same origin https requests, append idToken to header.
    if (self.location.origin == getOriginFromUrl(evt.request.url) &&
        (self.location.protocol == 'https:' ||
         self.location.hostname == 'localhost') &&
        idToken) {
      // Clone headers as request headers are immutable.
      const headers = new Headers();
      req.headers.forEach((val, key) => {
        headers.append(key, val);
      });
      // Add ID token to header.
      headers.append('Authorization', 'Bearer ' + idToken);
      processRequestPromise = getBodyContent(req).then((body) => {
        try {
          req = new Request(req.url, {
            method: req.method,
            headers: headers,
            mode: 'same-origin',
            credentials: req.credentials,
            cache: req.cache,
            redirect: req.redirect,
            referrer: req.referrer,
            body,
            // bodyUsed: req.bodyUsed,
            // context: req.context
          });
        } catch (e) {
          // This will fail for CORS requests. We just continue with the
          // fetch caching logic below and do not pass the ID token.
        }
      });
    }
    return processRequestPromise.then(() => {
      return fetch(req);
    });
  };
  // Fetch the resource after checking for the ID token.
  // This can also be integrated with existing logic to serve cached files
  // in offline mode.
  evt.respondWith(getIdToken().then(requestProcessor, requestProcessor));
});

Somit wird für alle authentifizierten Anfragen immer ein ID-Token übergeben. ohne weitere Verarbeitung eingeben zu müssen.

Damit der Service Worker Auth-Statusänderungen erkennen kann, muss er die auf der Anmeldeseite installiert sind. Achten Sie darauf, dass der Service Worker ist gebündelt, sodass er auch dann noch funktioniert, wenn der Browser geschlossen.

Nach der Installation wird der Dienst Worker muss bei der Aktivierung clients.claim() aufrufen, damit er eingerichtet werden kann als Controller für die aktuelle Seite.

Web

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

Web

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

Clientseitige Änderungen

Der Service Worker, sofern unterstützt, muss auf der Clientseite installiert werden Anmelde-/Registrierungsseite.

Web

// Install servicerWorker if supported on sign-in/sign-up page.
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/service-worker.js', {scope: '/'});
}

Web

// Install servicerWorker if supported on sign-in/sign-up page.
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/service-worker.js', {scope: '/'});
}

Wenn der Nutzer angemeldet und zu einer anderen Seite weitergeleitet wird, kann das ID-Token in den Header einschleusen, bevor die Weiterleitung abgeschlossen ist.

Web

import { getAuth, signInWithEmailAndPassword } from "firebase/auth";

// Sign in screen.
const auth = getAuth();
signInWithEmailAndPassword(auth, email, password)
  .then((result) => {
    // Redirect to profile page after sign-in. The service worker will detect
    // this and append the ID token to the header.
    window.location.assign('/profile');
  })
  .catch((error) => {
    // Error occurred.
  });

Web

// Sign in screen.
firebase.auth().signInWithEmailAndPassword(email, password)
  .then((result) => {
    // Redirect to profile page after sign-in. The service worker will detect
    // this and append the ID token to the header.
    window.location.assign('/profile');
  })
  .catch((error) => {
    // Error occurred.
  });

Serverseitige Änderungen

Der serverseitige Code kann das ID-Token bei jeder Anfrage erkennen. Dieses wird vom Admin SDK für Node.js oder beim Web unterstützt. SDK, das FirebaseServerApp verwendet.

Node.js

  // Server side code.
  const admin = require('firebase-admin');

  // The Firebase Admin SDK is used here to verify the ID token.
  admin.initializeApp();

  function getIdToken(req) {
    // Parse the injected ID token from the request header.
    const authorizationHeader = req.headers.authorization || '';
    const components = authorizationHeader.split(' ');
    return components.length > 1 ? components[1] : '';
  }

  function checkIfSignedIn(url) {
    return (req, res, next) => {
      if (req.url == url) {
        const idToken = getIdToken(req);
        // Verify the ID token using the Firebase Admin SDK.
        // User already logged in. Redirect to profile page.
        admin.auth().verifyIdToken(idToken).then((decodedClaims) => {
          // User is authenticated, user claims can be retrieved from
          // decodedClaims.
          // In this sample code, authenticated users are always redirected to
          // the profile page.
          res.redirect('/profile');
        }).catch((error) => {
          next();
        });
      } else {
        next();
      }
    };
  }

  // If a user is signed in, redirect to profile page.
  app.use(checkIfSignedIn('/'));

Web modular API

import { initializeServerApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
import { headers } from 'next/headers';
import { redirect } from 'next/navigation';

export default function MyServerComponent() {

    // Get relevant request headers (in Next.JS)
    const authIdToken = headers().get('Authorization')?.split('Bearer ')[1];

    // Initialize the FirebaseServerApp instance.
    const serverApp = initializeServerApp(firebaseConfig, { authIdToken });

    // Initialize Firebase Authentication using the FirebaseServerApp instance.
    const auth = getAuth(serverApp);

    if (auth.currentUser) {
        redirect('/profile');
    }

    // ...
}

Fazit

Da ID-Tokens über die Service Worker festgelegt werden Worker dürfen nur vom selben Ursprung aus ausgeführt werden, es besteht kein Risiko von CSRF da eine Website unterschiedlichen Ursprungs versucht, Ihre Endpunkte aufzurufen, den Service Worker nicht aufrufen können, sodass die Anfrage aus Sicht des Servers nicht authentifiziert sind.

Service Worker werden mittlerweile von allen gängigen Browsern unterstützt. Einige werden von älteren Browsern nicht unterstützt. Daher kann es sein, dass einige Fallbacks erforderlich, um das ID-Token an Ihren Server zu übergeben, wenn Service Worker nicht oder eine App kann so eingeschränkt werden, dass sie nur in Browsern Service Workers.

Beachten Sie, dass Service Worker nur einen Ursprung haben und nur installiert werden. auf Websites, die über eine HTTPS-Verbindung oder localhost bereitgestellt werden.

Weitere Informationen zur Browserunterstützung für Service Worker finden Sie unter caniuse.com