Firebase Security Rules

Myohzx
Sep 5 · 12 min read
Image Credits

If you know about Firebase, then you probably have heard about Firebase Security Rules at some point.

Firebase rules are present in Firebase databases (Realtime Database and Cloud Firestore) and Cloud Firestore and can help protect your data.

Because Firebase is a cloud data storage that is accessible directly from any connected client, it is important to restrict the data you do not want a client to access.

For example, you would like any user to be able to create an account in your app, but you certainly would not want any user to be able to edit or delete all reviews in your database, even those provided by other users.

In this post, I will cover and exemplify security rules with Cloud Firestore and Firebase Storage syntax.


Syntax

Before moving forward it is important to understand the syntax of Firebase security rules. In both Cloud Firestore and Firebase Storage, the rules consist of match statements and allow expressions.

Match statements are responsible for identifying the path in the database or storage location to be accessed.

Allow expressions are the conditions we use to control the access to the documents specified in the match statement.

In the following figure, you can compare the syntax of security rules in Cloud Firestore (left) and Firebase Storage (right).

In both cases, the first line states what type of service we are using (Cloud Firestore on the left and Firebase Storage on the right).

The second line should not be changed and points to the root of your database/storage location.

Finally, from the third to the fifth line is presented the syntax we should use to create Firebase security rules. You have a match statement that specifies where the rule should be applied (a collection on the database or a path in storage location) and inside it, you have the allow expression, that specifies the rule itself.

Example of Firebase security rules on Cloud Firestore (left) and Firebase Storage (right)

Match Statements

There are two types of match statements. One of them is the match /{document=**} statement (as shown in the previous image). The =** symbol refers to anything nested after the path we had already specified. In this particular case (example above) we are not specifying any particular collection as the path in the previous match statement. This means we are pointing to the root of the database. Therefore, every document (from any collection) in our database will be covered by this security rule. The same situation will occur in the Firebase Storage example above with the match /{allPaths=**} statement.

Note: This match statement will apply to all documents and all sub-collection documents.

The other type of match statements are the ones where we actually specify a document or a set of documents in a collection.

Imagine you need to apply rules to only one specific user. For this, you will need to specify the collection with the information you want to protect, and then the user’s id. Thus, we can specify a particular document as follows: match /User/test@gmail.com .

User is the name of the Collection of our database and test@gmail.com is the id we decided to use to distinguish each user. In this case the id is the email of the user, but can also be a set of randomly generated characters.

However, most of the time you will never really know the users who will eventually use your app neither their emails so this is not a common solution used to create security rules.

Instead, we may need to create rules for all users of a given collection. For this, we use what is called wild cards. If we continue with the example of the User collection we would need something like this: match /User/{userId} . Instead of entering the hard-coded id, we open brackets and give it a name of our choice which will be the variable name that matches the id of any document within the collection at run-time. This will cover all documents (user information) in the User collection.

Summary:

To summarize, suppose your app is about allowing users to create posts about restaurants and attractions and also allowing other users to review them.

In the example below, we have a match statement created in Cloud Firestore that covers the Reviews collection and covers all of the documents inside it.

service cloud.firestore {
match /databases/{database}/documents {
match Reviews/{review} {
...
}

}
}

In the example below, we have a match statement created in Firebase Storage. On this Firebase Storage example there is a global folder ( postImages ) where the post images are saved. Every time a user creates a post about a restaurant and uploads an image of that restaurant a new folder with the id of the user (email) is going to be created inside the previous folder postImages.

So basically the match statement bellow (match /postImages/{userId}/{allPaths=**} ) will cover all of the restaurant images a specific user uploads on the app.

service firebase.storage {
match /b/{bucket}/o {
match /postsImages/{userId}/{allPaths=**} {
...
}

}
}

Allow Expressions

Another component of a security rule is the allow expression. These expressions will dictate the conditions required to access the information in our database and/or Firebase Storage.

In both Cloud Firestore and Firebase Storage there are two main operations to use in allow expressions: read and write .

  • The read operation is used when we want to condition the user from reading data.
  • The write operation is used for when we want to condition the user from writing data to the database or to Firebase Storage (like creating, editing or deleting a document/file).

In Firebase Storage you can only use the read and write operations. But in Cloud Firestore these operations can be divided into several sub-operations.

Below are the sub-operations you can use on Cloud Firestore. The first two can be combined in the read operation, while the other three can be combined in the write operation:

Read sub-operations:

get — for when a user makes a read request of a single document.

list — for when a user makes a read request of an entire collection.

Write sub-operations:

create— for creating a new document.

update— for updating a document that already exists.

delete— for deleting a document.

Note: It is very common to combine theget and list operations into read action. However, each situation is unique and therefore you may need a different solution.

Any request made by a user of our app to Cloud Firestore or Firebase Storage is evaluated against their security rules before reading or writing data. If the rules deny access to any of the specified document paths, the entire request will fail.


Practical Examples

Now that we know the basics, it’s important to give some of the more common examples of firebase security rules.

Cloud Firestore and Firebase Storage rules

The following rules examples can be applied to both Cloud Firestore and Firebase Storage.

Note: Remember that in Firebase Storage you cannot split read and write read and write actions.

1. Deny reads and writes to all users

This is the security rule that Firebase shows by default. However, this rule must be changed before the app can be used by other users.

service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
}
}

In this example, we are using the Cloud Firestore service. The match statement is pointing to the root of the database, so whatever the allow expression is, it will apply to all documents in every single collection of your database.

This rule makes it impossible for any user to read or create data. Thus, this rule is really only necessary for development purposes.

2. Allow reads/writes for all users

Use this security rule with caution as its not very secure to allow reads/writes to all users in every document of every collection of your database/storage.

service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if true;
}
}
}

Another way of writing this rule is simply by writing allow read, write; (since it does not specify a condition, it is the same as its value is true).

3. Allow writes to signed-in users

Imagine you have a collection in your database named Review. In this collection, you store information about reviews made to posts on your app. Normally you would only want to allow reviews (create new reviews, edit or delete them) from a user who is logged in your app.

For this, you need to make a request to Firebase Authentication by writing request.auth and then you verify if its value is not null. If it returns null then the user is not able to write reviews. If it returns not null then the user is signed in and therefore, can create new reviews, delete and update them (documents on the database, under the collection Reviews).

service cloud.firestore {
match /databases/{database}/documents {
match Reviews/{review} {
allow write: if request.auth != null;
}
}
}

Note that for this to work you need to have activated Firebase Authentication.

4. Allow writes to users with verified email

You may want to verify if the user’s email has been verified before letting the user create or make changes to your database /Firebase Storage.

So, after configuring and programming your app to send the user an email verification in the registration process you just have to specify the rule request.auth.token.email_verified.

If the user has already verified his/her email, then the result of the condition is true and the user can (in this case) create, edit or delete documents from the Review collection in the database.

service cloud.firestore {
match /databases/{database}/documents {
match Reviews/{review} {
allow write: if request.auth.token.email_verified;
}
}
}

If the email has not been verified, then the user will not be able to make any modifications to the database.

5. Allow deleting documents only if they were created by the user himself

Even if we condition our user’s actions by verifying if they are logged in to the app or if their email has been verified, it is sometimes important to only let the users change documents if they were created by themselves.

Let’s say you have a collection in your database named Post. In this collection, you store information about restaurants and attractions that users in the app can create to share with each other. By creating a post of a restaurant, a document is created in the collection Post. This document will have a lot of information (like the name’s restaurant, the location, and so on). Apart from this, each created document will have an id that binds it to its owner/creator (idOwner).

If you use Firebase Authentication (you must!), then all users registered in your application will have a user identifier (email) and a user UID. Now let’s say that the idOwner attribute of the document is going to be the email of the user who created it.

If we just want to allow a user to delete their own posts, we need to make sure that the identifier (email) in the Firebase Authentication record of the user trying to delete a post is the same as the idOwner present in the post trying to be deleted. For this you use request.auth.token.email == request.resorce.data.idOwner. With this, you are making a request to Firebase Authintication (or Firebase Auth) to return you the email of the user who is trying to make this action (request.auth.token.email) and a request to your database in the Collection Post in order to retrieve the value of the idOwner attribute (request.resorce.data.idOwner).

service cloud.firestore {
match /databases/{database}/documents {
match /Post/{postId} {
allow delete: if request.auth.token.email
== request.resource.data.idOwner
;
}
}
}

If this comparison returns true, then the user shall delete the post. Otherwise, that will not be possible because the post that the user is trying to delete its not their own.

6. Deny document writes to anonymous accounts

With Firebase authentication, you can choose from several login methods: common login with email and password, login with google account, login with twitter account and more. One of these options is the anonymous login.

Imagine you want any user to be able to see your home activity (in a mobile app) to see what the app looks like, but you can’t do anything else. This is an example where you can make a user login with an anonymous account. In your code, you will want to check if the user is using an anonymous account and condition the user to certain actions if so. However, security rules will still need to be made to ensure, for example, that an anonymous user cannot create documents in the app.

For this, you will need to do a request to Firebase Authentication to return which login provider the user is using at the time of accessing the application. So you just have to compare the request request.auth.token.firebase.sign_in_provider with the string “anonymous”.

service cloud.firestore {
match /databases/{database}/documents {
match /Post/{postId} {
allow read: if request.auth.token.firebase.sign_in_provider
== "anonymous"
;
allow write: if request.auth.token.firebase.sign_in_provider
!= "anonymous"
;
}
}
}

If the condition returns true, then the user is in fact logged in without an actual account and you can allow them to read the information, but not to create new documents (as this requires an account).

Firebase Storage Rules (only)

In Firebase Storage, there are some rules you can create that cannot be used in Firebase databases. These rules are related to file uploads.

1. Only allow upload of image bellow 25 MB

If the users can upload files in your app then it is a good idea to restrict the size of those files. For that you just need to write the following rule:

service firebase.storage {
match /b/{bucket}/o {
match /postsImages/{userId}/{allPaths=**} {
allow read: if request.resource.size < 25 * 1024 * 1024;
}
}
}

2. Allow only one type of file extension

Once again, if the user is able to upload files in your app it is better to make sure these files are something you expect in your app.

For this purpose, you can make sure if the resource the user is about to upload is or not an image with extension .png.

service firebase.storage {
match /b/{bucket}/o {
match /postsImages/{userId}/{allPaths=**} {
allow read: if request.resource.contentType.matches(‘image/png’);
}
}
}

If the resource that the user is trying to upload is not an image or has not the .png extension, then the user will not gonna be able to upload that file. On the contrary, if the resource is an image AND has the .png extension, then there will be no problem in uploading the file.


Operators

I already used the operators > , == , * , != in this post, but you can do much more with operators.

The operators || and && in Firebase security rules are important because you are likely to need to apply various conditions to an allow expression within a rule.

For example, as in the previous example, we want to ensure that the user can upload only one image. However, unlike the previous example, you do not want the user to have only the option to upload one image, but rather multiple image extensions.

service firebase.storage {
match /b/{bucket}/o {
match /postsImages/{userId}/{allPaths=**} {
allow read: if request.auth != null &&
request.auth.token.email_verified ||
request.auth.token.firebase.sign_in_provider == "anonymous";

allow write: if
request.resource.contentType.matches(‘image/png’) ||
request.resource.contentType.matches('image/jpeg') ||
request.resource.contentType.matches('image/jpg');
}
}
}

You can achieve that with operators. In this case, you just need to make all the conditions and separate them with the || (OR) operator. This way you give the user more options regarding the different file extensions that can be uploaded.

See the official documentation to learn more about the existing operators.


Functions

In Firebase security rules, you also create functions. These are very useful for better organizing your rules when we start to use the same conditions for multiple rules.

These functions must be placed inside the root match statement (match /databases/{database}/documents in Cloud Firestore and match /b/{bucket}/o in Firebase Storage), but outside of any other match statement.

service firebase.storage {
match /b/{bucket}/o {
match /postsImages/{userId}/{allPaths=**} {

allow read: if userIsSignedIn() &&
emailIsVerified() ||
userIsAnonymous();
allow write: if userIsSignedIn() &&
emailIsVerified();

} //ends match statement
//FUNCTIONS
//User is signed in
function userIsSignedIn() {
return request.auth != null;
}

//User has his email verified
function emailIsVerified() {
return request.auth.token.email_verified;
}

//User is anonymous
function userIsAnonymous() {
return request.auth.token.firebase.sign_in_provider == "anonymous";
}

}
}

You start the creation of a function just like Javascript by writing function followed by a name, followed by parentheses (). The code to be executed, by the function, is placed inside curly brackets {}. Inside it write return ( to return the value to the allow expression you assigned it) and then the condition you would just like in the allowexpression.

Then you call the function’s name in the allow expression where you need it to be.


Debugging Tips

In your Firebase console when you are writing your security rules you can simulate and test various reads and writes requests.

You can simulate the path of your Firebase Storage or the Collections and documents in Cloud Firestore, an authentication type (anonymous user, authenticated, not authenticated, …) or even a specific document with specific data. This will help you to easily replicate the testing environment and see if your rules protect your data before publishing your security rules.

The result of the simulation appear at the top of the editor where you should see a banner confirming the read/write was either successfully allowed or denied.


Myohzx

Written by

Myohzx

Web and Android Developer Enthusiast

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade