Firebase Auth, oturum yönetimi için Firebase kimlik jetonlarını algılamak ve iletmek üzere hizmet çalışanlarını kullanma olanağı sunar. Bu durum aşağıdaki avantajları sağlar:
- Sunucudan gelen her HTTP isteğinde ek bir işlem yapmadan kimlik jetonu iletme olanağı.
- Kimlik jetonunu ek gidiş dönüş veya gecikme olmadan yenileyebilme.
- 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.
- Firebase Auth kaynak kodunun her sayfaya eklenmesi ihtiyacını ortadan kaldırır (gecikmeyi azaltır). Bir kez yüklenip başlatılan hizmet çalışanı, oturum yönetimini tüm istemciler için arka planda gerçekleştirir.
Genel Bakış
Firebase Auth, istemci tarafında çalışacak şekilde optimize edilmiştir. Jetonlar web depolama alanına kaydedilir. Bu sayede Realtime Database, Cloud Firestore ve Cloud Storage gibi diğer Firebase hizmetleriyle de kolayca entegre olabilirsiniz. Oturumları sunucu tarafında yönetmek için kimlik jetonlarının alınması ve sunucuya iletilmesi 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 bu jetonun istek üstbilgisi, POST gövdesi vb. aracılığıyla sunucuya iletilmesi gerektiği anlamına gelir.
Bu yöntem ölçeklenmeyebilir ve bunun yerine sunucu tarafı oturum çerezleri gerekebilir. Kimlik jetonları oturum çerezleri olarak ayarlanabilir ancak bunlar kısa ömürlüdür ve istemciden yenilenmesi, ardından süresi dolduğunda yeni çerezler olarak ayarlanması gerekir. Kullanıcı siteyi bir süredir ziyaret etmediyse bu işlem için ek bir gidiş-dönüş gerekebilir.
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, özellikle diğer istemci tabanlı Firebase hizmetlerini de kullanmanız gerekiyorsa istemci jetonları ve sunucu tarafı jetonları senkronize olamayacağından yönetimi daha zordur.
Bunun yerine, sunucu tarafında tüketim için kullanıcı oturumlarını yönetmek üzere hizmet çalışanları kullanılabilir. Bu durumun nedeni şunlardır:
- Service worker'lar, mevcut Firebase Auth durumuna erişebilir. Mevcut kullanıcı kimliği jetonu, hizmet çalışanından alınabilir. Jetonun süresi dolmuşsa istemci SDK'sı jetonu yeniler ve yeni bir jeton döndürür.
- Service worker'lar, getirme isteklerini yakalayıp değiştirebilir.
Hizmet çalışanı değişiklikleri
Kullanıcı oturum açtıysa hizmet çalışanının kimlik doğrulama kitaplığını ve 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 yapılan tüm getirme istekleri yakalanır ve bir kimlik jetonu varsa istek başlık aracılığıyla isteğe eklenir. Sunucu tarafında, istek başlıklarında kimlik jetonu 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)); });
Bu nedenle, kimliği doğrulanmış tüm isteklerde her zaman ek işlem yapılmadan üstbilgiye bir kimlik jetonu iletilir.
Service worker'ın kimlik doğrulama durumu değişikliklerini algılayabilmesi için oturum açma/kaydolma sayfasına yüklenmesi gerekir. Tarayıcı kapatıldıktan sonra da çalışmaya devam etmesi için hizmet çalışanının paketlendiğinden emin olun.
Yüklemeden sonra, hizmet çalışanının etkinleştirme sırasında clients.claim()'ı araması gerekir. Böylece, mevcut sayfa için denetleyici 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ı, istemci tarafındaki oturum açma/kaydolma sayfasına yüklenmelidir.
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, yönlendirme tamamlanmadan önce hizmet çalışanı, üstbilgiye kimlik jetonunu 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ı kodu, her istekte kimlik jetonunu algılayabilir. Bu davranış, Node.js için Admin SDK veya FirebaseServerApp kullanılarak Web SDK ile 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 modular 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 çalışanları aracılığıyla ayarlanacağından ve hizmet çalışanlarının aynı kaynaktan çalışması kısıtlandığından, farklı bir kaynaktan gelen bir web sitesi uç noktalarınızı çağırmaya çalıştığında hizmet çalışanını çağıramaz. Bu da isteğin sunucu açısından kimliği doğrulanmamış görünmesine neden olur. Dolayısıyla, CSRF riski yoktur.
Service worker'lar artık tüm modern tarayıcılarda destekleniyor olsa da bazı eski tarayıcılar bu özelliği desteklemez. Bu nedenle, hizmet çalışanları kullanılamadığında veya bir uygulamanın yalnızca hizmet çalışanlarını destekleyen tarayıcılarda çalışması kısıtlandığında kimlik jetonunu sunucunuza iletmek için bazı yedekler gerekebilir.
Hizmet çalışanlarının yalnızca tek kaynaklı olduğunu ve yalnızca https bağlantısı veya localhost üzerinden sunulan web sitelerine yükleneceğini unutmayın.
Hizmet çalışanı 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 çalışanlarını kullanma hakkında daha fazla bilgi edinmek istiyorsanız GitHub'daki örnek uygulama kaynak koduna göz atın.
- Yukarıdakilerin dağıtılmış bir örnek uygulamasına https://auth-service-worker.appspot.com adresinden ulaşabilirsiniz.