সেবা কর্মীদের সঙ্গে অধিবেশন ব্যবস্থাপনা

ফায়ারবেস অথোরাইজেশন (Firebase Auth) সেশন ম্যানেজমেন্টের জন্য ফায়ারবেস আইডি টোকেন শনাক্ত করতে এবং প্রেরণ করতে সার্ভিস ওয়ার্কার ব্যবহারের সুবিধা প্রদান করে। এর ফলে নিম্নলিখিত সুবিধাগুলো পাওয়া যায়:

  • কোনো অতিরিক্ত কাজ ছাড়াই সার্ভার থেকে প্রতিটি HTTP অনুরোধে একটি আইডি টোকেন পাঠানোর ক্ষমতা।
  • কোনো অতিরিক্ত রাউন্ড ট্রিপ বা বিলম্ব ছাড়াই আইডি টোকেন রিফ্রেশ করার ক্ষমতা।
  • ব্যাকএন্ড এবং ফ্রন্টএন্ডের মধ্যে সিঙ্ক্রোনাইজড সেশন। যেসব অ্যাপ্লিকেশনের রিয়েলটাইম ডেটাবেস, ফায়ারস্টোর ইত্যাদির মতো ফায়ারবেস সার্ভিস এবং কিছু এক্সটার্নাল সার্ভার সাইড রিসোর্স (যেমন SQL ডেটাবেস) অ্যাক্সেস করার প্রয়োজন হয়, তারা এই সমাধানটি ব্যবহার করতে পারে। এছাড়াও, একই সেশন সার্ভিস ওয়ার্কার, ওয়েব ওয়ার্কার বা শেয়ার্ড ওয়ার্কার থেকেও অ্যাক্সেস করা যায়।
  • প্রতিটি পৃষ্ঠায় ফায়ারবেস অথেন্টিকেশন সোর্স কোড অন্তর্ভুক্ত করার প্রয়োজনীয়তা দূর করে (লেটেন্সি কমায়)। একবার লোড ও ইনিশিয়ালাইজ হওয়া সার্ভিস ওয়ার্কারটি ব্যাকগ্রাউন্ডে সমস্ত ক্লায়েন্টের জন্য সেশন ম্যানেজমেন্ট পরিচালনা করবে।

সংক্ষিপ্ত বিবরণ

ফায়ারবেস অথোরাইজেশন ক্লায়েন্ট সাইডে চলার জন্য অপ্টিমাইজ করা হয়েছে। টোকেনগুলো ওয়েব স্টোরেজে সংরক্ষিত থাকে। এর ফলে রিয়েলটাইম ডেটাবেস, ক্লাউড ফায়ারস্টোর, ক্লাউড স্টোরেজ ইত্যাদির মতো অন্যান্য ফায়ারবেস পরিষেবাগুলোর সাথেও এটিকে সহজে ইন্টিগ্রেট করা যায়। সার্ভার সাইড থেকে সেশন পরিচালনা করার জন্য, আইডি টোকেনগুলো পুনরুদ্ধার করে সার্ভারে পাঠাতে হয়।

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

তবে, এর মানে হলো, সর্বশেষ আইডি টোকেনটি পাওয়ার জন্য ক্লায়েন্ট থেকে কোনো একটি স্ক্রিপ্ট চালাতে হবে এবং তারপর সেটি রিকোয়েস্ট হেডার, পোস্ট বডি ইত্যাদির মাধ্যমে সার্ভারে পাঠাতে হবে।

এই পদ্ধতিটি হয়তো কার্যকর নাও হতে পারে এবং এর পরিবর্তে সার্ভার-সাইড সেশন কুকির প্রয়োজন হতে পারে। আইডি টোকেনগুলোকে সেশন কুকি হিসেবে সেট করা যায়, কিন্তু এগুলো স্বল্পস্থায়ী এবং ক্লায়েন্ট থেকে রিফ্রেশ করার প্রয়োজন হবে। মেয়াদ শেষ হলে আবার নতুন কুকি হিসেবে সেট করতে হবে, যার জন্য একটি অতিরিক্ত রাউন্ড ট্রিপের প্রয়োজন হতে পারে যদি ব্যবহারকারী বেশ কিছুদিন ধরে সাইটটি ভিজিট না করে থাকেন।

যদিও 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);
      }
    });
  });
};

অ্যাপের অরিজিনে পাঠানো সমস্ত ফেচ রিকোয়েস্ট ইন্টারসেপ্ট করা হবে এবং যদি কোনো আইডি টোকেন পাওয়া যায়, তবে হেডারের মাধ্যমে তা রিকোয়েস্টের সাথে যুক্ত করা হবে। সার্ভার সাইডে, রিকোয়েস্ট হেডারগুলোতে আইডি টোকেনটি চেক, ভেরিফাই এবং প্রসেস করা হবে। সার্ভিস ওয়ার্কার স্ক্রিপ্টে, ফেচ রিকোয়েস্টটি ইন্টারসেপ্ট করে মডিফাই করা হবে।

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

এর ফলে, সমস্ত প্রমাণীকৃত অনুরোধের হেডারে কোনো অতিরিক্ত প্রক্রিয়াকরণ ছাড়াই সর্বদা একটি আইডি টোকেন পাঠানো হবে।

অথোরাইজেশন অবস্থার পরিবর্তন শনাক্ত করার জন্য সার্ভিস ওয়ার্কারকে সাইন-ইন/সাইন-আপ পেজে ইনস্টল করতে হবে। নিশ্চিত করুন যে সার্ভিস ওয়ার্কারটি বান্ডল করা আছে, যাতে ব্রাউজার বন্ধ করার পরেও এটি কাজ করতে থাকে।

ইনস্টলেশনের পরে, সার্ভিস ওয়ার্কারকে অ্যাক্টিভেশনের সময় 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.
  });

সার্ভার সাইডের পরিবর্তন

সার্ভার সাইড কোড প্রতিটি অনুরোধে আইডি টোকেনটি শনাক্ত করতে সক্ষম হবে। এই আচরণটি Node.js-এর জন্য অ্যাডমিন এসডিকে অথবা 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('/'));

ওয়েব মডিউলার এপিআই

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-এর কোনো ঝুঁকি নেই। এর কারণ হলো, ভিন্ন অরিজিনের কোনো ওয়েবসাইট আপনার এন্ডপয়েন্টগুলোকে কল করার চেষ্টা করলে তা সার্ভিস ওয়ার্কারকে চালু করতে ব্যর্থ হবে, যার ফলে সার্ভারের দৃষ্টিকোণ থেকে অনুরোধটি অপ্রমাণিত বলে মনে হবে।

যদিও সার্ভিস ওয়ার্কার এখন সব আধুনিক প্রধান ব্রাউজারে সমর্থিত, কিছু পুরোনো ব্রাউজার এগুলো সমর্থন করে না। ফলে, যখন সার্ভিস ওয়ার্কার উপলব্ধ থাকে না, তখন আপনার সার্ভারে আইডি টোকেন পাঠানোর জন্য কোনো ফলব্যাকের প্রয়োজন হতে পারে অথবা কোনো অ্যাপকে শুধুমাত্র সার্ভিস ওয়ার্কার সমর্থনকারী ব্রাউজারগুলিতে চলার জন্য সীমাবদ্ধ করা যেতে পারে।

উল্লেখ্য যে, সার্ভিস ওয়ার্কারগুলো শুধুমাত্র সিঙ্গেল অরিজিন ভিত্তিক এবং এগুলো কেবল https কানেকশন বা লোকালহোস্টের মাধ্যমে পরিবেশিত ওয়েবসাইটগুলিতেই ইনস্টল করা হবে।

সার্ভিস ওয়ার্কারের জন্য ব্রাউজার সাপোর্ট সম্পর্কে আরও জানতে caniuse.com দেখুন।