Introduction
Firebase est une plateforme MBaaS (Mobile Backend as a Service) incontournable pour le développement mobile et web. Proposée par Google, elle centralise des outils essentiels qui facilitent le déploiement et la gestion des applications, de l’hébergement (Hosting) à l'authentification (Authentication) des utilisateurs, en passant par les bases de données en temps réel (Firestore). Firebase simplifie ainsi le travail des développeurs tout en optimisant l’expérience et l’engagement des utilisateurs.
Dans notre cas, Firebase a été choisi pour servir de backend à notre application mobile, offrant ainsi une solution complète et évolutive pour gérer l'ensemble des services nécessaires.
Présentation de la situation
Afin de vous présenter Firebase ainsi que la mise en place des règles de sécurité, imaginons une application de restauration:
- Chaque client peut créer et éditer ses propres commandes.
- Les restaurateurs peuvent consulter toutes les commandes en modifiant uniquement le statut.
Pour stocker les données relatives aux commandes, nous utilisons Firestore, une base de données NoSQL flexible et évolutive. Côté application mobile, le SDK cloud_firestore (Package Flutter dans notre application) est intégré pour manipuler facilement les données de Firestore.
Problème actuel
En utilisant le SDK cloud_firestore, voici comment on récupère les commandes spécifiques à un utilisateur:
final collection = db.jsonCollectionWithId("orders",
from: Program.fromFirebase,
to: (value) => value.toJson,
);
final userOrders = collection.where('authorId', isEqualTo: uid).snapshots();
La partie qui gère le filtrage des données qui concernent l’utilisateur est le :
where(‘authorId’, isEqualTo: uid)
Toutefois, il faut se rappeler que même si le code de l’application est obfusqué, il reste accessible et modifiable, ce qui peut représenter une vulnérabilité ! On ne veut pas qu’un utilisateur ait accès à toutes les commandes des autres utilisateurs en modifiant le code.
Ainsi, il est nécessaire de protéger non seulement le code client (application), mais également de sécuriser le serveur et les données en utilisant des mécanismes de sécurité robustes.
Il va donc falloir protéger notre Firestore. Pour ce faire, il faut utiliser les Règles de Sécurité Firestore. Ces règles garantissent que chaque utilisateur n’accède qu’aux informations auxquelles il est autorisé, et protégeront la modification de ces mêmes informations.
Comment ça fonctionne ?
Les règles de sécurité Firebase servent à gérer qui peut voir ou modifier les données dans votre base de données ou votre stockage. Elles permettent de protéger les informations sensibles et de contrôler précisément les accès.
De plus, les règles Firebase peuvent être rendues plus complexes et sécurisées, par exemple en implémentant des rôles pour les utilisateurs. Bien que ces rôles soient optionnels, ils sont recommandés, car ils apportent beaucoup de valeur en attribuant uniquement les droits nécessaires.
Pour expliquer comment fonctionnent les règles Firebase, nous allons avoir une collection orders et une collection users en se basant sur notre application de restauration, à noter que les documents de nos collections sont tous identifiés grâce à un ID unique :
Collection : Orders
ID du document : "order_id"
Contenu du document :
{
"name":"Fried Chicken",
"price":7,
"currency":"EUR",
"command_at":1722414161,
"delivery_address":"5 rue des Volontaires, 75015",
"status":"ORDERED", // ORDERED, DELIVERED
"user_id":"c0357b2e-7451-49cd-be3c-9fd4696ad48a"
}
Collection : Users
ID du document : "user_id"
Contenu du document :
{
"firstname":"Jack",
"lastname":"Sparrow",
"role":"CUSTOMER", //CUSTOMER ou RESTAURATEUR
}
Présentation de l’éditeur
Le Firestore Database de votre projet contient une partie Règles.
La règle par défaut de chaque projet est :
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if true;
}
}
}
Pour expliquer brièvement, le service cloud firestore est obligatoire pour indiquer quel service est concerné par ces règles (dans le cas présent : Le Cloud Firestore).
Le premier match précise qu’il s’adresse à l’ensemble des documents de la database, et le match suivant cible tous les documents (avec le wildcard **)
Par défaut, la règle autorise tout le monde à lire et écrire sur tous les documents de notre Firestore, ce qui n'est pas adapté pour des applications en production. Pour mieux comprendre comment gérer ces règles, explorons différents cas pratiques.
Voyons comment fonctionnent les règles de sécurité Firestore en les appliquant sur orders/{id}.
Pour écrire une règle de sécurité sur Firebase, il est essentiel de connaître les différentes actions possibles :
Read | Write |
---|---|
get | create |
list | delete |
update |
On peut voir deux actions principales : read et write. Ces actions regroupent plusieurs sous-actions afin de simplifier la gestion des autorisations :
- Read : Englobe toutes les actions liées à la consultation des données, que ce soit des documents ou des collections complètes
- Write : Regroupe toutes les actions de modification de données, comme la création, la suppression et la mise à jour de documents
Les sous-actions permettent de gérer les permissions de manière plus précise et robuste, car elles donnent accès à une seule action spécifique sans accorder de droits supplémentaires. Cet avantage est particulièrement utile dans des situations concrètes, notamment pour des utilisateurs avec des rôles ou statuts spécifiques, qui ne doivent pas disposer de tous les droits d'écriture ou de lecture.
match /orders/{id} {
allow read: if true;
allow write: if true;
}
Le code ci-dessus équivaut au code ci-dessous:
match /orders/{id} {
// Actions de lecture
allow get: if true;
allow list: if true;
// Actions d'écriture
allow create: if true;
allow update: if true;
allow delete: if true;
}
Vérifier l’authentification de l’utilisateur
Afin de protéger correctement notre collection, il est important d’envisager toutes les possibilités d’accès pour pouvoir autoriser ou restreindre en fonction de la situation. Dans le cas de notre collection Orders, dans un premier temps, seuls les utilisateurs authentifiés peuvent y avoir accès.
Nous allons réaliser cela grâce à Firebase Authentication, un autre outil de Firebase.
Dans notre application, les utilisateurs s’authentifient via Firebase Authentication, un outil qui gère l’authentification de l’application de A à Z, de la création de compte avec un email/mot de passe jusqu’à l’authentification via Google ou Apple, en passant par la réinitialisation de mot de passe par mail ou SMS.
En combinant Firebase Authentication avec notre Firestore, nous pouvons vérifier si un utilisateur est authentifié lors de sa requête. Voici comment mettre en place cette vérification:
match /orders/{id} {
allow read, write: if request.auth != null;
}
Dans cette règle, on peut voir que la condition if request.auth != null s’applique sur les actions read et write.
Limiter l’utilisateur à accéder à ses propres données
Limiter l’accès aux commandes personnelles
Nous voulons restreindre l'accès aux commandes pour que chaque utilisateur ne puisse consulter que ses propres commandes, garantissant une gestion sécurisée des données. L'idée est de n'autoriser l'accès aux commandes que si elles appartiennent à l'utilisateur connecté. Pour cela, nous utilisons resource.data pour accéder aux champs du document, sous la forme de clé/valeur.
Il faut donc ajuster nos règles pour vérifier deux conditions:
- L'utilisateur est connecté
- La commande lui appartient
Chaque commande possède un champ user_id. Ce champ se trouve dans la variable resource.data. Voici la nouvelle règle:
match /orders/{id} {
allow read: if request.auth != null && request.auth.uid == resource.data.user_id;
allow write: if request.auth != null && request.auth.uid == resource.data.user_id;
}
⚠️ Attention : Les règles de sécurité ne filtrent pas les données, elles définissent uniquement les autorisations d'accès.
Par exemple, la requête suivante sera rejetée, car elle tente de récupérer toutes les commandes, y compris celles qui n'appartiennent pas à l'utilisateur connecté :
db.collection(“orders”).get(); //pas de where dans l’appel
En revanche, cette requête sera acceptée car elle ne récupère que les commandes de l'utilisateur connecté, respectant ainsi nos règles établies:
db.collection(“orders”).where(“user_id”, isEqualTo: uid).get(); //Le where filtre la recherche, et respecte la règle
Amélioration avec des fonctions
Dans la règle précédente, nous avons dupliqué les conditions pour la lecture et l'écriture, ce qui va à l'encontre du principe DRY (Don't Repeat Yourself). Avec les règles de sécurité Firebase, nous pouvons définir des fonctions pour éviter cette répétition.
Les fonctions Firebase fonctionnent comme dans d'autres langages : elles prennent des paramètres mais ne renvoient que des booléens. Nous allons créer deux fonctions : isUserAuthenticated et isUserCommand.
function isUserAuthenticated() {
return request.auth != null;
}
function isUserCommand(orderUserId) {
return request.auth.uid == orderUserId;
}
// Maintenant, créons une fonction pour regrouper les deux fonctions précédentes
function isUserAuthenticatedAndUserCommand() {
return isUserAuthenticated() && isUserCommand(resource.data.user_id);
}
Nous pouvons ensuite utiliser cette fonction dans nos règles :
match /orders/{id} {
allow read, write: if isUserAuthenticatedAndUserCommand();
}
Implémenter un système de rôle
Gestion des rôles pour les restaurateurs
Actuellement, chaque utilisateur ne peut consulter que ses propres commandes. Nous devons maintenant étendre cette fonctionnalité pour permettre aux restaurateurs de voir l'ensemble des commandes. Pour cela, nous allons implémenter un système de rôles:
- Un utilisateur avec le rôle "CUSTOMER", qui pourra uniquement consulter ses propres commandes
- Un restaurateur avec un rôle "RESTAURATEUR", qui pourra accéder à toutes les commandes.
Approche du système de rôles
Une option serait d’utiliser des custom tokens via le SDK Firebase Admin pour gérer les rôles au niveau de l'authentification. Cependant, comme notre exemple n'utilise pas ce SDK, nous allons stocker les rôles directement dans Firestore, en ajoutant un champ rôle à la collection User.
Voici un exemple de document dans la collection User:
Collection: users
Document ID: "user_id"
{
firstname: "Jack",
lastname: "Sparrow",
role: "CUSTOMER" // ou "RESTAURATEUR"
}
Modification des règles
Pour vérifier le rôle de l’utilisateur, il faut dans un premier temps récupérer son document, identifié par son ID d’utilisateur:
let userDoc = get(/databases/$(database)/documents/users/$(request.auth.uid));
⚠️ Attention : Cette requête de lecture peut affecter votre quota, alors assurez-vous de consulter la documentation Firebase sur les limites d'utilisation.
Ensuite, nous créons une fonction pour vérifier si l'utilisateur a les droits appropriés :
function checkRole(userDoc, role) {
return userDoc != null && userDoc.data.role == role;
}
function isUserCustomerAndIsUserCommand(userDoc) {
return checkRole(userDoc, “CUSTOMER”) &&
isUserCommand(resource.data.user_id);
}
function canUserReadOrder() {
let userDoc = get(/databases/$(database)/documents/users/$(request.auth.uid));
return isUserAuthenticated() && (isUserCustomerAndIsUserCommand(userDoc) || checkRole(userDoc, “RESTAURATEUR”));
}
Cette fonction vérifie si un utilisateur “CUSTOMER” accède à ses propres commandes ou si un utilisateur est un “RESTAURATEUR”, auquel cas il n’a pas de restrictions sur la lecture de commandes. Vous pouvez ajuster ces règles selon vos besoins, par exemple en restreignant les restaurateurs à leurs propres restaurants.
Mise à jour de la règle "read" pour la collection orders
Voici la règle modifiée prenant en compte les rôles :
match /orders/{id} {
allow read: if canUserReadOrder();
allow write: if isUserAuthenticatedAndUserCommand();
}
Compléter l’édition d’une donnée
Gestion des actions d'écriture en fonction des rôles
Pour conclure, nous allons ajuster les permissions d’écriture des utilisateurs en fonction de leur rôle, tout en limitant l'accès aux différents champs des documents.
Actuellement, le privilège write regroupe toutes les actions d’écriture, ce qui pose problème : à la fois les clients et les restaurateurs peuvent effectuer toutes les opérations sans distinction. Or, un client doit pouvoir créer et annuler ses commandes, mais ne doit pas pouvoir modifier leur statut, tandis qu'un restaurateur peut uniquement mettre à jour le statut des commandes sans en créer ou en annuler.
Spécification des permissions
Nous allons remplacer write par des permissions plus spécifiques : create, update et delete.
match /orders/{id} {
allow read: ifcanUserReadOrder();
allow create, delete: if isUserAuthenticatedAndUserCommand();
allow update: if isUserAuthenticatedAndUserCommand(); //A modifier pour autoriser uniquement les restaurateurs
}
Vérification des rôles
Le deuxième changement consiste à utiliser la fonction checkRole pour distinguer les droits du client et du restaurateur. Le client peut créer ou supprimer ses commandes, tandis que le restaurateur peut uniquement les mettre à jour. Pour cela, il n’est pas nécessaire que la commande appartienne au restaurateur connecté.
function canCustomerWriteOrder(userDoc) {
return isUserAuthenticatedAndUserCommand() && checkRole(userDoc, “CUSTOMER”);
}
match /orders/{id} {
allow read: if canUserReadOrder();
allow create, delete: if canCustomerWriteOrder(get(/databases/$(database)/documents/users/$(request.auth.uid)));
allow update: if isUserAuthenticatedAndUserCommand();
}
Gestion des mises à jour
La mise à jour des commandes varie selon le rôle. Le client peut modifier tous les champs sauf le statut, tandis que le restaurateur peut changer uniquement le statut. Pour cela, nous créons deux fonctions : canClientUpdate et canRestaurateurUpdate, qui s’appuient sur les fonctions diff() et affectedKeys() pour vérifier les champs modifiés.
function canClientUpdate(userDoc) {
return canCustomerWriteOrder(userDoc) && !request.resource.data.diff(resource.data).affectedKeys().hasAny(“status”);
}
function canRestaurateurUpdate(userDoc) {
return checkRole(userDoc, “RESTAURATEUR”) && request.resource.data.diff(resource.data).affectedKeys().hasOnly(“status”);
}
function canUserUpdate() {
let userDoc = get(/databases/$(database)/documents/users/$(request.auth.uid));
return isUserAuthenticated() && canClientUpdate(userDoc) || canRestaurateurUpdate(userDoc);
}
Règles finales
Voici la version finale des règles de sécurité pour les commandes :
match /orders/{id} {
allow read: if canUserReadOrder();
allow create, delete: if canCustomerWriteOrder(get(/databases/$(database)/documents/users/$(request.auth.uid)));
allow update: if canUserUpdate();
}
Conclusion
À présent, vous avez toutes les armes pour pouvoir créer vos propres règles !
Les règles de sécurité Firebase sont essentielles pour protéger les données et gérer les accès. Vous pouvez contrôler les droits des utilisateurs, garantissant ainsi une application sécurisée.