Usługa Firebase Auth umożliwia korzystanie z usług workera w celu wykrywania i przekazywania tokenów Firebase ID na potrzeby zarządzania sesją. Dzięki temu:
- Możliwość przekazywania tokena identyfikacyjnego w każdym żądaniu HTTP z serwera bez konieczności wykonywania dodatkowych czynności.
- możliwość odświeżania tokena identyfikacyjnego bez dodatkowych opóźnień;
- Sesje zsynchronizowane z backendem i frontendem. Z tego rozwiązania mogą korzystać aplikacje, które potrzebują dostępu do usług Firebase, takich jak baza danych czasu rzeczywistego czy Firestore, oraz niektórych zewnętrznych zasobów po stronie serwera (baza danych SQL itp.). Do tej samej sesji można też uzyskać dostęp z poziomu instancji roboczej usługi, instancji roboczej przeglądarki lub współdzielonej instancji roboczej.
- Uniemożliwia wyświetlanie kodu źródłowego funkcji uwierzytelniania Firebase na każdej stronie (zmniejsza opóźnienie). Service worker, który został załadowany i jednorazowo zainicjowany, będzie zarządzać sesją dla wszystkich klientów w tle.
Omówienie
Usługa Firebase Auth jest zoptymalizowana do działania po stronie klienta. Tokeny są zapisywane w pamięci sieciowej. Ułatwia to też integrację z innymi usługami Firebase, takimi jak Baza danych czasu rzeczywistego, Cloud Firestore czy Cloud Storage. Aby zarządzać sesjami po stronie serwera, tokeny identyfikatora muszą zostać pobrane i przekazane na serwer.
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. });
Oznacza to jednak, że na kliencie musi być uruchomiony jakiś skrypt, który pobierze najnowszy token ID, a potem przekaże go na serwer za pomocą nagłówka żądania, treści żądania POST itp.
Może to nie działać na dużą skalę, dlatego konieczne mogą być pliki cookie sesji po stronie serwera. Tokeny identyfikacyjne mogą być ustawiane jako pliki cookie sesji, ale są one krótkotrwałe i trzeba je odświeżać z klienta, a potem ustawiać jako nowe pliki cookie po wygaśnięciu. Może to wymagać dodatkowej wymiany danych, jeśli użytkownik nie odwiedzał witryny od dłuższego czasu.
Uwierzytelnianie Firebase zapewnia bardziej tradycyjne rozwiązanie do zarządzania sesją na podstawie plików cookie, które najlepiej sprawdza się w przypadku aplikacji httpOnly
po stronie serwera. Jest ono trudniejsze do zarządzania, ponieważ tokeny po stronie klienta i serwera mogą się rozsynchronizować, zwłaszcza jeśli musisz też korzystać z innych usług Firebase po stronie klienta.
Zamiast tego można używać usług workera do zarządzania sesjami użytkowników po stronie serwera. Dzieje się tak, ponieważ:
- Serwisy mają dostęp do bieżącego stanu Uwierzytelniania Firebase. Bieżący token identyfikatora użytkownika można pobrać z serwisowego workera. Jeśli token wygasł, pakiet SDK klienta odświeży go i zwróci nowy.
- Usługa robocza może przechwycić żądania pobierania i je modyfikować.
Zmiany w skryptach service worker
Usługa workera musi zawierać bibliotekę Auth i możliwość uzyskania bieżącego tokena tożsamości, jeśli użytkownik jest zalogowany.
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); } }); }); };
Wszystkie żądania pobierania do źródła aplikacji będą przechwytywane, a jeśli dostępny jest token ID, zostanie on dołączony do żądania w nagłówku. Po stronie serwera nagłówki żądań zostaną sprawdzone pod kątem tokena tożsamości, zweryfikowane i przetworzone. W skrypcie service worker żądanie pobierania zostanie przechwycone i zmodyfikowane.
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)); });
W rezultacie wszystkie uwierzytelnione żądania będą zawsze zawierać w nagłówku token identyfikacyjny bez dodatkowego przetwarzania.
Aby usługa workera wykrywała zmiany stanu uwierzytelniania, musi być zainstalowana na stronie logowania lub rejestracji. Upewnij się, że proces roboczy w tle jest zgrupowany, aby działał nawet po zamknięciu przeglądarki.
Po zainstalowaniu pracownik usługi musi wywołać funkcję clients.claim()
podczas aktywacji, aby można było skonfigurować go jako kontroler bieżącej strony.
Web
self.addEventListener('activate', (event) => { event.waitUntil(clients.claim()); });
Web
self.addEventListener('activate', (event) => { event.waitUntil(clients.claim()); });
Zmiany po stronie klienta
Skrypt service worker (jeśli jest obsługiwany) musi być zainstalowany po stronie klienta na stronie logowania lub rejestracji.
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: '/'}); }
Gdy użytkownik zaloguje się i zostanie przekierowany na inną stronę, skrypt service worker będzie mógł wstrzyknąć token identyfikacyjny w nagłówku przed zakończeniem przekierowania.
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. });
Zmiany po stronie serwera
Kod po stronie serwera będzie mógł wykrywać token identyfikatora w każdym żądaniu. Takie działanie jest obsługiwane przez pakiet Admin SDK dla Node.js lub przez pakiet Web SDK za pomocą interfejsu FirebaseServerApp
.
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('/'));
Interfejs API modularny w internecie
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');
}
// ...
}
Podsumowanie
Dodatkowo tokeny identyfikatora są ustawiane za pomocą usług działających w tle, a usługi te mogą działać tylko w ramach tego samego źródła. Nie ma więc ryzyka związanego z atakiem CSRF, ponieważ próba wywołania punktów końcowych przez witrynę z innego źródła spowoduje niepowodzenie wywołania usługi działającej w tle, co spowoduje, że żądanie będzie wyglądać na niezaufanego z perspektywy serwera.
Chociaż usługowe pliki workera są obecnie obsługiwane we wszystkich nowoczesnych przeglądarkach, niektóre starsze przeglądarki ich nie obsługują. W związku z tym może być konieczne użycie rozwiązania zastępczego, aby przekazać token identyfikatora na serwer, gdy usługa service worker jest niedostępna. Możesz też ograniczyć działanie aplikacji tylko do przeglądarek obsługujących usługi service worker.
Pamiętaj, że skrypty service worker mają tylko jedno pochodzenie i będą instalowane tylko na stronach internetowych udostępnianych za pomocą połączenia https lub localhost.
Więcej informacji o obsługiwaniu skryptów service worker w przeglądarkach znajdziesz na stronie caniuse.com.
Przydatne linki
- Więcej informacji o używaniu usług dla pracowników w celu zarządzania sesjami znajdziesz w przykładowym kodzie źródłowym aplikacji na GitHubie.
- Rozwiniętą przykładową aplikację opisaną powyżej można znaleźć na stronie https://auth-service-worker.appspot.com.