Firebase Auth, oturum yönetimi için Firebase kimlik jetonlarını algılayıp iletmek üzere hizmet işçilerini kullanma olanağı sunar. Bu durum, aşağıdaki avantajları sunar:
- Ek çalışma yapmadan sunucudan gelen her HTTP isteğinde bir kimlik jetonu iletme olanağı.
- Ek gidiş geliş veya gecikmeler olmadan kimlik jetonunu yenileme olanağı.
- Arka uç ve ön uç senkronize oturumları. Realtime Database, Firestore 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 hizmet çalışanı, web çalışanı veya paylaşılan çalışandan da erişilebilir.
- Her sayfaya Firebase Auth kaynak kodu ekleme ihtiyacını ortadan kaldırır (gecikmeyi azaltır). Bir kez yüklenen ve başlatılan hizmet çalışanı, arka planda tüm istemciler için oturum yönetimini üstlenir.
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 gibi diğer Firebase hizmetleriyle entegrasyonu da kolaylaştırır. Oturumları sunucu tarafı açısından yönetmek için kimlik jetonlarının alınması ve sunucuya aktarılması gerekir.
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. });
Ancak bu, en son kimlik jetonunu almak için istemciden bir komut dosyasının çalıştırılması ve ardından istek üstbilgisi, POST gövdesi vb. aracılığıyla sunucuya iletilmesi gerektiği anlamına gelir.
Bu yöntem ölçeklenebilir olmayabilir ve bunun yerine sunucu tarafı oturum çerezleri gerekebilir. Kimlik jetonları oturum çerezleri olarak ayarlanabilir ancak bunların süresi kısadır ve istemciden yenilenmesi, ardından süresi dolduktan sonra yeni çerezler olarak ayarlanması gerekir. Bu işlem, kullanıcı siteyi bir süredir ziyaret etmemişse ek bir gidiş dönüş gerektirebilir.
Firebase Auth daha geleneksel bir çerez tabanlı oturum yönetimi çözümü sunsa da bu çözüm, sunucu tarafı httpOnly
çerez tabanlı uygulamalar için en iyi sonucu verir. Ayrıca, istemci jetonları ve sunucu tarafı jetonları senkronize olmayabileceğinden, özellikle de istemci tabanlı diğer Firebase hizmetlerini de kullanmanız gerekiyorsa bu çözümün yönetimi daha zordur.
Bunun yerine, sunucu tarafı tüketim için kullanıcı oturumlarını yönetmek üzere hizmet çalışanları kullanılabilir. Bunun nedeni şudur:
- Hizmet çalışanları, mevcut Firebase Auth durumuna erişebilir. Geçerli kullanıcı kimliği jetonu, servis çalışanından alınabilir. Jetonun süresi dolmuşsa istemci SDK'sı jetonu yeniler ve yeni bir jeton döndürür.
- Hizmet çalışanları, getirme isteklerini durdurup değiştirebilir.
Hizmet çalışanı değişiklikleri
Hizmet çalışanının, kimlik doğrulama kitaplığını ve kullanıcı oturum açtıysa geçerli kimlik jetonunu alma özelliğini içermesi gerekir.
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); } }); }); };
Uygulamanın kaynağına gönderilen tüm getirme istekleri engellenir ve bir kimlik jetonu varsa başlık aracılığıyla isteğe eklenir. Sunucu tarafında, istek başlıklarında kimlik jetonu olup olmadığı kontrol edilir, doğrulanır ve işlenir. Hizmet çalışanı komut dosyasında, getirme isteği yakalanır ve değiştirilir.
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)); });
Sonuç olarak, kimliği doğrulanmış tüm isteklerde, ek işleme gerek kalmadan başlıkta her zaman bir kimlik jetonu gönderilir.
Hizmet çalışanının kimlik doğrulama durumu değişikliklerini algılaması için oturum açma/kaydolma sayfasına yüklenmesi gerekir. Hizmet çalışanının, tarayıcı kapatıldıktan sonra çalışmaya devam etmesi için paketlendiğinden emin olun.
Yüklemeden sonra, hizmet işleyicinin etkinleştirildiğinde clients.claim()
'ü çağırması gerekir. Böylece, mevcut sayfanın denetleyicisi olarak ayarlanabilir.
Web
self.addEventListener('activate', (event) => { event.waitUntil(clients.claim()); });
Web
self.addEventListener('activate', (event) => { event.waitUntil(clients.claim()); });
İstemci tarafı değişiklikleri
Destekleniyorsa hizmet çalışanının istemci taraflı oturum açma/kaydolma sayfasına yüklenmesi gerekir.
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: '/'}); }
Kullanıcı oturum açtığında ve başka bir sayfaya yönlendirildiğinde hizmet çalışanı, yönlendirme tamamlanmadan önce kimlik jetonunu başlığa ekleyebilir.
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. });
Sunucu tarafı değişiklikleri
Sunucu tarafı kod, her istekte kimlik jetonunu algılayabilir. Bu davranış, Node.js için Yönetici SDK'sı veya FirebaseServerApp
kullanılarak Web SDK'sı tarafından desteklenir.
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 modüler 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');
}
// ...
}
Sonuç
Ayrıca, kimlik jetonları hizmet işçileri aracılığıyla ayarlandığı ve hizmet işçileri aynı kaynaktan çalışacak şekilde kısıtlandığı için CSRF riski yoktur. Bunun nedeni, farklı bir kaynaktan gelen ve uç noktalarınızı çağırmaya çalışan bir web sitesinin hizmet işçisini çağıramamasıdır. Bu da istek, sunucunun bakış açısından kimliği doğrulanmamış olarak görünmesine neden olur.
Hizmet çalışanları artık tüm modern büyük tarayıcılarda destekleniyor olsa da bazı eski tarayıcılarda desteklenmez. Bu nedenle, hizmet işçileri kullanılamadığında kimlik jetonunu sunucunuza iletmek için bazı yedek çözümler gerekebilir veya bir uygulama yalnızca hizmet işçilerini destekleyen tarayıcılarda çalışacak şekilde kısıtlanabilir.
Hizmet çalışanlarının yalnızca tek kaynaklı olduğunu ve yalnızca https bağlantısı veya yerel ana makine üzerinden yayınlanan web sitelerine yükleneceğini unutmayın.
Hizmet çalışanları için tarayıcı desteği hakkında daha fazla bilgiyi caniuse.com adresinde bulabilirsiniz.
Faydalı bağlantılar
- Oturum yönetimi için hizmet işçilerini kullanma hakkında daha fazla bilgi edinmek isterseniz GitHub'daki örnek uygulama kaynak koduna göz atın.
- Yukarıdaki örnek uygulamayı https://auth-service-worker.appspot.com adresinden dağıtabilirsiniz.