مدیریت جلسات با کارکنان خدمات

Firebase Auth امکان استفاده از service workerها را برای شناسایی و ارسال توکن‌های Firebase ID برای مدیریت session فراهم می‌کند. این امر مزایای زیر را به همراه دارد:

  • امکان ارسال یک توکن شناسه در هر درخواست HTTP از سرور بدون هیچ کار اضافی.
  • امکان به‌روزرسانی توکن شناسه بدون هیچگونه رفت و برگشت یا تأخیر اضافی.
  • جلسات هماهنگ‌شده‌ی بک‌اند و فرانت‌اند. برنامه‌هایی که نیاز به دسترسی به سرویس‌های فایربیس مانند Realtime Database، Firestore و غیره و برخی منابع سمت سرور خارجی (پایگاه داده SQL و غیره) دارند، می‌توانند از این راهکار استفاده کنند. علاوه بر این، می‌توان به همان جلسه از طریق سرویس ورکر، وب ورکر یا اشتراکی نیز دسترسی داشت.
  • نیاز به درج کد منبع Firebase Auth در هر صفحه را از بین می‌برد (تاخیر را کاهش می‌دهد). سرویس ورکر، که یک بار بارگذاری و مقداردهی اولیه می‌شود، مدیریت جلسه را برای همه کلاینت‌ها در پس‌زمینه انجام می‌دهد.

نمای کلی

احراز هویت فایربیس برای اجرا در سمت کلاینت بهینه شده است. توکن‌ها در فضای ذخیره‌سازی وب ذخیره می‌شوند. این امر ادغام آن با سایر سرویس‌های فایربیس مانند Realtime Database، Cloud Firestore، Cloud Storage و غیره را نیز آسان می‌کند. برای مدیریت جلسات از دیدگاه سمت سرور، توکن‌های شناسه باید بازیابی و به سرور ارسال شوند.

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

با این حال، این بدان معناست که برخی اسکریپت‌ها باید از سمت کلاینت اجرا شوند تا آخرین توکن شناسه را دریافت کنند و سپس آن را از طریق هدر درخواست، بدنه POST و غیره به سرور منتقل کنند.

این ممکن است مقیاس‌پذیر نباشد و در عوض ممکن است به کوکی‌های جلسه سمت سرور نیاز باشد. توکن‌های شناسه را می‌توان به عنوان کوکی‌های جلسه تنظیم کرد، اما این کوکی‌ها کوتاه‌مدت هستند و باید از طرف کلاینت به‌روزرسانی شوند و سپس در صورت انقضا به عنوان کوکی‌های جدید تنظیم شوند که اگر کاربر مدتی از سایت بازدید نکرده باشد، ممکن است به یک رفت و برگشت اضافی نیاز داشته باشد.

اگرچه Firebase Auth یک راهکار مدیریت نشست سنتی‌تر مبتنی بر کوکی ارائه می‌دهد، اما این راهکار برای برنامه‌های مبتنی بر کوکی httpOnly سمت سرور بهترین عملکرد را دارد و مدیریت آن دشوارتر است زیرا توکن‌های کلاینت و توکن‌های سمت سرور می‌توانند از همگام‌سازی خارج شوند، به‌خصوص اگر نیاز به استفاده از سایر سرویس‌های Firebase مبتنی بر کلاینت نیز داشته باشید.

در عوض، می‌توان از سرویس ورکرها برای مدیریت جلسات کاربر برای مصرف سمت سرور استفاده کرد. این روش به دلایل زیر کار می‌کند:

  • سرویس ورکرها به وضعیت فعلی احراز هویت فایربیس دسترسی دارند. توکن شناسه کاربر فعلی را می‌توان از سرویس ورکرها بازیابی کرد. اگر توکن منقضی شده باشد، SDK کلاینت آن را به‌روزرسانی کرده و یک توکن جدید برمی‌گرداند.
  • سرویس ورکرها می‌توانند درخواست‌های واکشی را رهگیری کرده و آنها را تغییر دهند.

تغییرات سرویس ورکر

سرویس ورکر باید کتابخانه Auth و قابلیت دریافت توکن شناسه فعلی را در صورت ورود کاربر، لحاظ کند.

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

تمام درخواست‌های واکشی به مبدا برنامه رهگیری می‌شوند و اگر شناسه توکن (ID token) موجود باشد، از طریق هدر به درخواست اضافه می‌شود. سمت سرور، هدرهای درخواست برای شناسه توکن بررسی، تأیید و پردازش می‌شوند. در اسکریپت سرویس ورکر، درخواست واکشی رهگیری و اصلاح می‌شود.

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

در نتیجه، تمام درخواست‌های احراز هویت شده همیشه یک شناسه توکن (ID token) در هدر خود بدون پردازش اضافی ارسال می‌کنند.

برای اینکه سرویس ورکر بتواند تغییرات وضعیت احراز هویت را تشخیص دهد، باید در صفحه ورود/ثبت‌نام نصب شود. مطمئن شوید که سرویس ورکر به صورت بسته‌بندی شده است تا پس از بسته شدن مرورگر، همچنان کار کند.

پس از نصب، سرویس ورکر باید در زمان فعال‌سازی، clients.claim() را فراخوانی کند تا بتواند به عنوان کنترلر برای صفحه فعلی تنظیم شود.

Web

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

Web

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

تغییرات سمت کلاینت

در صورت پشتیبانی، سرویس ورکر باید در صفحه ورود/ثبت‌نام سمت کلاینت نصب شود.

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

وقتی کاربر وارد سیستم می‌شود و به صفحه دیگری هدایت می‌شود، سرویس ورکر می‌تواند قبل از تکمیل هدایت، توکن شناسه را در هدر تزریق کند.

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

تغییرات سمت سرور

کد سمت سرور قادر خواهد بود توکن شناسه را در هر درخواست شناسایی کند. این رفتار توسط Admin SDK برای Node.js یا با Web SDK با استفاده از FirebaseServerApp پشتیبانی می‌شود.

نود جی اس

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

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

    // ...
}

نتیجه‌گیری

علاوه بر این، از آنجایی که توکن‌های شناسه از طریق سرویس ورکرها تنظیم می‌شوند و سرویس ورکرها محدود به اجرا از یک مبدا هستند، هیچ خطر CSRF وجود ندارد زیرا وب‌سایتی با مبدا متفاوت که سعی در فراخوانی نقاط انتهایی شما دارد، در فراخوانی سرویس ورکرها ناموفق خواهد بود و باعث می‌شود درخواست از دیدگاه سرور احراز هویت نشده به نظر برسد.

در حالی که سرویس ورکرها اکنون در همه مرورگرهای اصلی مدرن پشتیبانی می‌شوند، برخی از مرورگرهای قدیمی‌تر از آنها پشتیبانی نمی‌کنند. در نتیجه، ممکن است زمانی که سرویس ورکرها در دسترس نیستند یا می‌توان یک برنامه را محدود به اجرا فقط در مرورگرهایی کرد که از سرویس ورکرها پشتیبانی می‌کنند، به مقداری fallback برای ارسال توکن ID به سرور شما نیاز باشد.

توجه داشته باشید که سرویس ورکرها فقط تک‌منبع هستند و فقط روی وب‌سایت‌هایی که از طریق اتصال https یا localhost ارائه می‌شوند، نصب می‌شوند.

برای اطلاعات بیشتر در مورد پشتیبانی مرورگر از سرویس ورکر، به caniuse.com مراجعه کنید.