Firebase Authentication permet d'utiliser des services workers pour détecter et transmettre des jetons d'ID Firebase à des fins de gestion des sessions. Cela offre les avantages suivants:
- Possibilité de transmettre un jeton d'ID à chaque requête HTTP du serveur sans effort supplémentaire.
- Possibilité d'actualiser le jeton d'ID sans aller-retour ni latence supplémentaires.
- Sessions synchronisées entre le backend et le frontend. Les applications qui doivent accéder aux services Firebase tels que Realtime Database, Firestore, etc. et certaines ressources côté serveur externe (base de données SQL, etc.) peuvent utiliser cette solution. De plus, vous pouvez également accéder à la même session à partir du service worker, du web worker ou du worker partagé.
- Vous n'avez plus besoin d'inclure le code source Firebase Auth sur chaque page (cela réduit la latence). Une fois chargé et initialisé, le service worker gère la gestion des sessions pour tous les clients en arrière-plan.
Présentation
Firebase Auth est optimisé pour s'exécuter côté client. Les jetons sont enregistrés dans le stockage Web. Cela permet également d'intégrer facilement d'autres services Firebase tels que Realtime Database, Cloud Firestore, Cloud Storage, etc. Pour gérer les sessions du point de vue du serveur, les jetons d'ID doivent être récupérés et transmis au serveur.
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. });
Toutefois, cela signifie qu'un script doit s'exécuter à partir du client pour obtenir le dernier jeton d'ID, puis le transmettre au serveur via l'en-tête de la requête, le corps POST, etc.
Cette approche peut ne pas être évolutive et des cookies de session côté serveur peuvent être nécessaires. Les jetons d'ID peuvent être définis en tant que cookies de session, mais ils ont une durée de vie courte et doivent être actualisés à partir du client, puis définis en tant que nouveaux cookies à l'expiration, ce qui peut nécessiter un aller-retour supplémentaire si l'utilisateur n'a pas visité le site depuis un certain temps.
Bien que Firebase Auth fournisse une solution de gestion des sessions basée sur les cookies plus traditionnelle, cette solution fonctionne mieux pour les applications côté serveur basées sur les cookies httpOnly
et est plus difficile à gérer, car les jetons client et les jetons côté serveur peuvent se désynchroniser, en particulier si vous devez également utiliser d'autres services Firebase côté client.
À la place, les services workers peuvent être utilisés pour gérer les sessions utilisateur pour la consommation côté serveur. Cela fonctionne pour les raisons suivantes:
- Les services workers ont accès à l'état actuel de Firebase Auth. Le jeton d'ID utilisateur actuel peut être récupéré à partir du service worker. Si le jeton a expiré, le SDK client l'actualise et en renvoie un nouveau.
- Les services workers peuvent intercepter les requêtes de récupération et les modifier.
Modifications apportées aux services workers
Le service worker doit inclure la bibliothèque Auth et la possibilité d'obtenir le jeton d'ID actuel si un utilisateur est connecté.
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); } }); }); };
Toutes les requêtes de récupération à l'origine de l'application seront interceptées et, si un jeton d'ID est disponible, ajoutées à la requête via l'en-tête. Côté serveur, les en-têtes de requête sont vérifiés pour le jeton d'ID, validés et traités. Dans le script du service worker, la requête de récupération est interceptée et modifiée.
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)); });
Par conséquent, un jeton d'ID est toujours transmis dans l'en-tête de toutes les requêtes authentifiées, sans traitement supplémentaire.
Pour que le service worker puisse détecter les modifications de l'état d'authentification, il doit être installé sur la page de connexion/inscription. Assurez-vous que le service worker est groupé afin qu'il continue de fonctionner après la fermeture du navigateur.
Après l'installation, le worker de service doit appeler clients.claim()
lors de l'activation afin qu'il puisse être configuré en tant que contrôleur pour la page actuelle.
Web
self.addEventListener('activate', (event) => { event.waitUntil(clients.claim()); });
Web
self.addEventListener('activate', (event) => { event.waitUntil(clients.claim()); });
Modifications côté client
Le service worker, s'il est compatible, doit être installé côté client sur la page de connexion/inscription.
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: '/'}); }
Lorsque l'utilisateur est connecté et redirigé vers une autre page, le service worker peut injecter le jeton d'ID dans l'en-tête avant la fin de la redirection.
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. });
Modifications côté serveur
Le code côté serveur pourra détecter le jeton d'identification à chaque requête. Ce comportement est compatible avec le SDK Admin pour Node.js ou avec le SDK Web à l'aide de FirebaseServerApp
.
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('/'));
API modulaire Web
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');
}
// ...
}
Conclusion
De plus, comme les jetons d'ID sont définis via les services workers et que ceux-ci ne peuvent s'exécuter qu'à partir de la même origine, il n'y a aucun risque de CSRF, car un site Web d'une autre origine qui tente d'appeler vos points de terminaison ne parviendra pas à appeler le service worker, ce qui entraînera l'affichage de la requête comme non authentifiée du point de vue du serveur.
Bien que les services workers soient désormais compatibles avec tous les principaux navigateurs modernes, certains navigateurs plus anciens ne le sont pas. Par conséquent, un mécanisme de remplacement peut être nécessaire pour transmettre le jeton d'ID à votre serveur lorsque les services workers ne sont pas disponibles ou qu'une application ne peut s'exécuter que sur des navigateurs compatibles avec les services workers.
Notez que les services workers ne sont d'origine unique que et ne seront installés que sur les sites Web diffusés via une connexion HTTPS ou localhost.
Pour en savoir plus sur la compatibilité des navigateurs avec les service workers, consultez caniuse.com.
Liens utiles
- Pour en savoir plus sur l'utilisation des service workers pour la gestion des sessions, consultez le code source de l'application exemple sur GitHub.
- Un exemple d'application déployée est disponible à l'adresse https://auth-service-worker.appspot.com.