Firebase Firestore Security Rules Basics

With Firebase, you can perform user authentication and perform database actions entirely from your frontend code. This also means our database is exposed to the client any hacker would easily gain access to the private data of our apps, right? Well no, that’s where Firestore rules come in and fix this open hole (mmmm??) in our database. These security rules can be applied to the Firebase real-time database, cloud firestore, and storage.


Introduction

Traditionally, you write your server which authenticates the user with tokens and validates that session on every request to your database, that’s pretty similar to security rules but you don’t have to write, run, and maintain your backend infrastructure to do so, Google and firebase will do that for you.

You can just write policies to define who has access to what in your database using an easy-to-learn language called CEL (Common Expression Language). Whenever a user requests your firestore database that request is routed through this policy where every request is denied by default and it looks for the first rule to allow it.

You can write these rules in the IDE or just directly in the Firebase Console. An added advantage of the Firebase console is that you’ll have access to the history of your previous rules from the past.

Before you write any actual conditions for your database here are some things you need to understand


Match

Before we write any rules we need to define where in the database a rule applies to, that represents a path to your data in your database. In a new project in Firebase, you’ll notice that by default there is a match block pointing to the root of your database.

this is a generic boilerplate code that puts you in the root of your database. Now there are three main types of matching patterns, single document match, collection match, or a hierarchy of collection and sub-collection match.

Single Document Match

Imagine you have a lot of users and each of them gets a unique uuid from Firebase authentication and you need to allow users to access only the data they own.

Here is how you can write a firestore rule to keep the data secure and accessible to the owner.

 match /users/{uid} {
    allow read, write: if uid == request.auth.uid
  }

Here you define that every “uid (variable)” in the “users” collection is to be allowed to read and write if the “uid” that is the document ID matches the uid provided by the firebase auth in the request made.

In short, you’re matching the document ID with the UID of the requesting user to check if he is allowed to access the collection or not.

Remember : The rules on parent collection doesn’t apply to sub collections of it

If you had an items collection nested under the user’s collection the user match rule doesn’t apply for that as it’s isolated with its own security policies.

You might want to apply the same rules for all the collections under a parent collection and that’s where recursive wild card is used.

Collection Match

 match /users/{docId=**} {
    allow read, write;
 }

In the above snippet if the user is allowed to read the user collection then it applies to all of its subcollections too. This way you don’t have to write a separate set of rules for all the collections under a parent collection.

If you want to define security rules to sub-collections then you can do that too.


match /users/{uid} {
   allow read, write: if uid == request.auth.uid
      
   match /users/{uid}/photos/{pid} {
     allow read, write;
   }
 }

in this case, you’re defining rules for the “photos” collection which is under the user’s (parent) collection.

Security is always excessive until it’s not enough


Allow – Read, Write, & Beyond

Every match block will contain two or more allowed blocks. I would like to take allow as a function that takes in two arguments, the first one is the operation that we want to allow, like read, write, update, or delete. The second one is a predicate or a boolean condition based on the application logic of your choice to allow a particular operation on that collection match.

Let’s look at an example

  match /users/{docId=**} {

      allow read, write;

      allow get;
      allow list;

      allow create;
      allow update;
      allow delete;
      
    }

Here with allow read, write you are allowing single and multiple document querying (reads) and creations (writes) to the user’s collection.

To get granular control over the actions allowed to be performed you can use allow get to allow querying of a single document and allow a list for multi-document read operations.

Now similarly, allow create allows them to only create the document in the collection and doesn’t allow them to perform modifications to it, and if you add allow update which allows modification of a document this is granular to the write condition. allow delete gives you control over who can delete a document in the collection.

For example

 match /users/{docId=**} {

      allow list, delete: true;
      
    }

This will allow people to query multiple documents in the collection and permit them to delete them.

Remember – Rules look for the FIRST allow, meaning if something is allowed before in the security rules cannot be unallowed in the below statements.

 match /users/{docId=**} {

      allow read, write:true;
      allow delete: false;
      
  }

In the above snippet, you already allowed full read and write access to the documents in the collections, so allow delete is obsolete in these rules.

Now that we know the fundamentals of allow, let’s look at conditions.


Conditions

Conditions are the backbone of firebase rules, which are statements that result in true or false. The Firebase rules environment contains a bunch of objects and helper methods that we can use to evaluate incoming requests, read other items in the database, check timestamps, and various other stuff.

Instead of overwhelming theory at once, let’s understand these with a handful of simple examples that you’ll very likely use in your projects.

The general syntax is similar to any modern programming language like javascript, python, basically any programming language. We can start by adding an if keyword and some kind of logic that evaluates to either true or false.

If we set the condition to false the operation will always be denied by the rules. This is the default behavior of the firestore database (In production mode).

In many cases, you will need to check multiple conditions before allowing an operation. For example, you might have to check if a user is logged in, has a timestamp property, and is the admin when performing a database read or a write.

You can use logical AND (&&) for checking multiple conditions called chained conditions. All the chained conditions must be true to allow operations.

Similarly, we have a logical OR (||) operator. In this case, only one of the condition must be true to allow the operation.

These are the conditions you’ll use the most but, there are bunch of other operators that are supported.

Now there is an important concept that you need to understand at this point which is request and resource data.

The Request represents the incoming data from the client-side application. That means the user from the client side is trying to read or write to the database and we have to evaluate the information in that request to allow or restrict the access.

request.auth – request.auth contains the JSON web token (jwt) information with the user’s authentication information from Firebase Authentication. That’s where you find the email address, name, uid, and custom claims that you set for a user.

request.resource – request.resource which is the actual data that the user is trying to write to the database. As an example, when a user tries to create a new document the resource will contain the actual payload that was sent from the client application.

Many other operations that you might need include

request.time – Timestamp of the request

request.path – Path to the document

request.method – Methods like read, write, delete, update…

These are pretty self-explanatory so I’ll skip a detailed explanation of these properties on the resource.

In addition to the request we also have a global object called “resource“. This represents the data that already exists in your database. Which might come in handy when the user is updating or deleting a document.

resource != request.resource

top-level resource is the object with your existing database, and request.resource is from the request user made

In this example, you are checking if the username in the existing database is the same as the username in the payload from the request sent from the client.

This rule checks if the user isn’t allowed to update something that cannot be updated.


Hope this gave you guys an overview of firestore rules. If you liked reading this or this helped you in any way that’s great for me. Thanks for reading.


Leave a Reply

Your email address will not be published. Required fields are marked *


This site uses Akismet to reduce spam. Learn how your comment data is processed.

You May Also Like