6 Must-Know Firestore Security Rules

   Raja Tamil • Aug 5 •

I have been asked by a lot of developers this one question…

Is Firebase safe as the config code is exposed to the browser? 🔥

My answer is Yes and No.

The config code that Firebase provides is meant to be public.

But…

The real security lays on the Security Rules. 👮‍♂️

Having the right security rule in place, you can build a secure web app with the Cloud Firestore without needing your own server at all.

However…

If you have your own server or are using cloud functions using Firebase Admin SDK, all security rules will be bypassed.

In that case, you will have to handle security manually in your server-side environment.

Let’s take a look at the SIX common scenarios that you may run into when writing Firestore Security Rules.

01. Read Permission For Anyone

Let’s say you have a posts collection and you want to show all documents from it to anyone who visits your site. You can do something like this:

service cloud.firestore {    
  match /databases/{database}/documents {      
    match /posts/{docId} {        
      allow read;     
    }    
  }  
} 

Pretty straightforward!

allow:read gives permission to read all documents in a given path, in this case, /posts/{docId}.

Other operations such as create, update and delete can be a different statement or merged with the read statement depending upon what type of rule you’re trying to write.

But, NEVER use allow:write without additional rules in place.

02. For Authenticated Users Only

Instead of showing the posts to anyone, how about showing them to only authenticated users.

service cloud.firestore {   
  match /databases/{database}/documents {     
    match /posts/{docId} {      
      allow read: if request.auth.uid != null     
    }   
  }
}

You can easily do that by checking to see if the signed-in user’s uid exists or not. Therequest.auth object has information about the signed-in user. So, you can access uid using request.auth.uid property.

Nice!

The third security rule will enforce only logged-in users can read or write their own documents, not others and vice-versa.

03. Secure Logged-In User Data

If you’re from an SQL background, this is a one-to-one relationship. One document per user in a users collection.

service cloud.firestore {    
  match /databases/{database}/documents {      
    match /users/{uid} {        
      allow read, write: if request.auth.uid == uid      
    }    
  }  
} 

In the inner match path, you can see the users collection slash the wildcard variable {uid} that basically represents any document that’s inside that collection.

Using this security rule, users can only update their own document if their logged-in user ID is equal to the uid that already exists in the database.

On top of that, you could add an email verification check as a second condition using request.auth.token.email_verified flag.

service cloud.firestore {    
  match /databases/{database}/documents {      
    match /users/{uid}/ {        
      allow read, write: 
        if request.auth.uid == uid &&
           request.auth.token.email_verified == true     
    }    
  }  
} 

04. Never Trust The Client And Validate Data

Let’s say you have a post collection and each document inside it has a title, content, author, author_id, etc. The author_id would be an actual user ID.

I often find that it’s easy to understand when showing the data structure before showing the security rule.

Sometimes, you will want to validate the incoming request data before saving into the database using the request object.

Let’s add a security rule to validate data before allowing users to add new posts to the Cloud Firestore.

service cloud.firestore {   
  match /databases/{database}/documents {     
    match /posts/{docId} {      
      allow create: 
        if request.resource.data.title is string && 
           request.data.title.size() > 5 &&
           request.data.title.size() < 500 &&
           request.resource.data.author_id == request.auth.uid     
     }   
  } 
}

It’s checking to see if the title is a string and also validating if the length of the title characters is in between 5 and 500 long.

As you can see, you can add validation pretty much to all of the fields, in fact it’s recommended because it gives more schema to your data model.

Finally, check to see if the request.resource.data.author_id is equal to the currently logged-in user’s uid.

This is very similar to server-side validation without managing your own server. 🙂

The Firestore query for adding a new post should be something like this:

firebase
  .firestore()     
  .collection("posts")     
  .add({         
      title: "JavaScript MVC",
      content: "In this JavaScript MVC...",     
      author: firebase.auth().currentUser.displayName, 
      author_id: firebase.auth().currentUser.uid     
   });

Check out my other article Learn Firestore CRUD Queries Quickly [Guide] if you want to know more about queries.

05. Get Multiple Documents Owned By A Single User.

Getting multiple documents by a single user often referred to a one-to-many relationship.

Before creating a rule, make sure that each document has a file called author_id which is basically the userId of whoever created that post.

match /posts/{docId} {    
  allow read, update: 
    if resource.data.author_id == request.auth.uid
 }

Using resource.data, you can access the fields of a stored document. Then, verify if the document is owned by the currently logged-in user by comparing author_id inside resource.data property with the currently logged-in user’s uid.

firebase
  .firestore()     
  .collection("posts")   
  .where("author_id", "==", firebase.auth().currentUser.uid)
  .get()     
  .then(snap => {        
     snap.forEach(doc => {             
       console.log(doc.data());         
     });     
  });

Here is the request query for updating data:

firebase     
  .firestore()   
  .collection("posts")  
  .doc("qIIz49uGTyHg737BYNSP")   
  .update({        
     title: "JavScript MVC Design Pattern"     
  });

06. Different Users Roles

One of the main use cases of security rules is to have different levels of permissions/roles for different users.

Let’s say you have a roles collection which determines which user has which role. This roles collection may be a root-level collection or a sub-collection of an existing document.

As you know, you can’t add a security rule to a partial document which is the indication that you will need to split it into two different documents so that each one can be targeted with different security rules.

There are two common ways you can add roles to the users.

Using custom auth claims, you can add different roles to users via Cloud Functions or your own server using Admin SDK.

Once it’s added, you can access the property using request.auth.token object.

match /posts/{docId} {   
  allow read, write: if request.auth.token.isAdmin 
}    

This rule can be combined with the existing ones like so.

match /posts/{docId} {      
  allow create: 
    if request.resource.data.title is string &&
       request.resource.data.title.size() > 5 &&        
       request.resource.data.title.size() < 500 &&
       request.resource.data.author_id == request.auth.uid
   allow read, update: if request.auth.uid == resource.data.author_id || 
       request.auth.token.isAdmin 
}

The other way is to create a sub-collection or create a top-level collection called roles.

As you can see, I have roles as a top-level collection. 

firestore-set-roles-permission
match /posts/{docId} {    
  allow read, write: if get(/databases/$(database)/documents/roles/$(request.auth.uid)).data.role["isAdmin"] 
}     

Here is how you can retrieve existing data from the database using get() method by passing the path as an argument. At the end of the path, I can access actual document fields using data property. 

Then, I am checking to see if the isAdmin role is true against that field. If yes, grant or write permission to any document in a posts collection to that user.

Another cool thing about security rule is that you can create reusable functions to avoid repeated code.

You can do something like this:

match /posts/{docId} {    
    allow read, write: if hasRole("isAdmin")  
}      

function hasRole(userRole) {    
  return get(/databases/$(database)/documents/roles/$(request.auth.uid)).data.role[userRole] 
}

NEXT → FirebaseUI Auth with Facebook, Google & Email for Vue.js

Sharing is caring!