Hizmet çalışanlarıyla oturum yönetimi

Firebase Auth, oturum yönetimi için Firebase ID belirteçlerini tespit etmek ve iletmek amacıyla hizmet çalışanlarını kullanma olanağı sağlar. Bu, aşağıdaki faydaları sağlar:

  • Sunucudan gelen her HTTP isteğinde herhangi bir ek çalışmaya gerek kalmadan bir kimlik belirtecinin iletilmesi yeteneği.
  • Kimlik belirtecini herhangi bir ek gidiş-dönüş veya gecikme olmaksızın yenileme yeteneği.
  • Arka uç ve ön uç senkronize oturumlar. Realtime Database, Firestore vb. gibi Firebase hizmetlerine ve bazı harici sunucu tarafı kaynaklarına (SQL veritabanı vb.) erişmesi gereken uygulamalar bu çözümü kullanabilir. Ayrıca aynı oturuma servis çalışanı, web çalışanı veya paylaşılan çalışandan da erişilebilir.
  • Her sayfaya Firebase Auth kaynak kodunu ekleme ihtiyacını ortadan kaldırır (gecikmeyi azaltır). Bir kez yüklenip başlatılan hizmet çalışanı, arka planda tüm istemciler için oturum yönetimini yönetecektir.

Genel Bakış

Firebase Auth, istemci tarafında çalışacak şekilde optimize edilmiştir. Jetonlar web depolama alanına kaydedilir. Bu, Realtime Database, Cloud Firestore, Cloud Storage vb. gibi diğer Firebase hizmetleriyle entegrasyonu da kolaylaştırır. Oturumları sunucu tarafı perspektifinden yönetmek için kimlik belirteçlerinin alınması ve sunucuya iletilmesi gerekir.

Web modular API

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 namespaced API

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

Ancak bu, en son kimlik belirtecini almak için bazı komut dosyalarının istemciden çalıştırılması ve ardından bunu istek başlığı, POST gövdesi vb. aracılığıyla sunucuya aktarması gerektiği anlamına gelir.

Bu ölçeklenmeyebilir ve bunun yerine sunucu tarafı oturum çerezleri gerekebilir. Kimlik belirteçleri, oturum çerezleri olarak ayarlanabilir, ancak bunlar kısa ömürlüdür ve istemciden yenilenmesi ve daha sonra, kullanıcı siteyi bir süre ziyaret etmemişse ek bir gidiş-dönüş gerektirebilecek süre sona erdiğinde yeni çerezler olarak ayarlanması gerekir.

Firebase Auth, daha geleneksel bir çerez tabanlı oturum yönetimi çözümü sağlarken, bu çözüm, sunucu tarafı httpOnly çerez tabanlı uygulamalar için en iyi şekilde çalışır ve istemci belirteçleri ile sunucu tarafı belirteçleri, özellikle de kullanmanız gerekiyorsa, senkronizasyon dışına çıkabileceğinden yönetimi daha zordur. diğer istemci tabanlı Firebase hizmetleri.

Bunun yerine hizmet çalışanları, sunucu tarafı tüketimine yönelik kullanıcı oturumlarını yönetmek için kullanılabilir. Bu, aşağıdakilerden dolayı işe yarar:

  • Hizmet çalışanlarının geçerli Firebase Auth durumuna erişimi vardır. Geçerli kullanıcı kimliği belirteci hizmet çalışanından alınabilir. Belirtecin süresi dolarsa istemci SDK'sı onu yenileyecek ve yeni bir tane döndürecektir.
  • Hizmet çalışanları, getirme isteklerini yakalayabilir ve bunları değiştirebilir.

Hizmet çalışanı değişiklikleri

Hizmet çalışanının, Kimlik Doğrulama kitaplığını ve bir kullanıcı oturum açmışsa geçerli kimlik belirtecini alma özelliğini eklemesi gerekir.

Web modular API

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 namespaced API

// 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);
      }
    });
  });
};

Uygulamanın kaynağına yapılan tüm getirme istekleri durdurulacak ve bir kimlik belirteci mevcutsa, başlık yoluyla isteğe eklenecektir. Sunucu tarafında istek başlıkları kimlik belirteci için kontrol edilecek, doğrulanacak ve işlenecektir. Service Worker komut dosyasında, getirme isteği durdurulacak ve değiştirilecektir.

Web modular API

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 namespaced API

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));
});

Sonuç olarak, kimliği doğrulanmış tüm isteklerde ek işleme gerek kalmadan her zaman başlıkta bir kimlik belirteci iletilecektir.

Hizmet çalışanının Kimlik Doğrulama durumu değişikliklerini algılaması için, bunun genellikle oturum açma/kaydolma sayfasına yüklenmesi gerekir. Kurulumdan sonra, hizmet çalışanının aktivasyon sırasında clients.claim() i çağırması gerekir, böylece mevcut sayfa için denetleyici olarak ayarlanabilir.

Web modular API

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

Web namespaced API

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

İstemci tarafı değişiklikleri

Destekleniyorsa, hizmet çalışanının istemci tarafındaki oturum açma/kaydolma sayfasına yüklenmesi gerekir.

Web modular API

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

Web namespaced API

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

Kullanıcı oturum açtığında ve başka bir sayfaya yönlendirildiğinde, hizmet çalışanı, yönlendirme tamamlanmadan önce kimlik belirtecini başlığa enjekte edebilecektir.

Web modular API

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 namespaced API

// 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.
  });

Sunucu tarafı değişiklikleri

Sunucu tarafı kodu, her istekte kimlik belirtecini algılayabilecektir. Bu, aşağıdaki Node.js Express örnek kodunda gösterilmektedir.

// Server side code.
const admin = require('firebase-admin');
const serviceAccount = require('path/to/serviceAccountKey.json');

// The Firebase Admin SDK is used here to verify the ID token.
admin.initializeApp({
  credential: admin.credential.cert(serviceAccount)
});

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('/'));

Çözüm

Ayrıca, kimlik belirteçleri hizmet çalışanları aracılığıyla belirleneceğinden ve hizmet çalışanlarının aynı kaynaktan çalıştırılması kısıtlandığından, uç noktalarınızı aramaya çalışan farklı kökenli bir web sitesi hizmet çalışanını çağırmayı başaramayacağından CSRF riski yoktur. isteğin sunucunun bakış açısına göre kimliği doğrulanmamış görünmesine neden olur.

Servis çalışanları artık tüm modern ana tarayıcılarda desteklense de, bazı eski tarayıcılar bunları desteklememektedir. Sonuç olarak, hizmet çalışanları mevcut olmadığında veya bir uygulama yalnızca hizmet çalışanlarını destekleyen tarayıcılarda çalışacak şekilde kısıtlanabildiğinde, kimlik belirtecini sunucunuza iletmek için bazı geri dönüşlere ihtiyaç duyulabilir.

Hizmet çalışanlarının yalnızca tek kökenli olduğunu ve yalnızca https bağlantısı veya localhost aracılığıyla sunulan web sitelerine yükleneceğini unutmayın.

caniuse.com adresinden servis çalışanına yönelik tarayıcı desteği hakkında daha fazla bilgi edinin.