Search Lessons, Code Snippets, and Videos
search by algolia
X
#native_cta# #native_desc# Sponsored by #native_company#

Firestore Security Rules Guide

Episode 92 written by Jeff Delaney
full courses for pro members

In this lesson, I will give you an in-depth guide to Firestore security rules. My goal is to make your rules code both maintainable and readable. We will go through a long list of common scenarios and write functions that will keep your code dry and readable.

Bookmark the Firestore Rules Reference.

How to Hack a Firebase App

Your Firebase app can hacked with ease if you fail to setup security rules. In the graphic below, I am simply sending a DELETE request using cURL with an Firebase app’s database URL.

Hacking firestore from the command line

Your database URL is available in the frontend app, so a hacker can grab it from the Chrome network tab (or in the frontend source code). If backend rules are not present, anybody can send destructive requests to this endpoint and potentially wipe out all of your data.

Fortunately, backend rules in Firebase are relatively easy to configure. This lesson provides a collection of Firestore rules snippets and functions that you can drop into your app.

Security Rules Basics

Let’s quickly run through some of the most basic security rule concepts. You can define rules directly in the Firebase console or from your IDE Firebase Tools CLI. We are going to build out rules that look something like this:

Firestore rules from the firebase console.

Scope Rules to Specific Operations

Rules can be enforced on various read/write operations that occur in a clientside app. We can scope rules to each of the follow read operations.

  • allow read - Applies to both lists and documents.
  • allow get - When reading a single document.
  • allow list - When querying a collection.

Write operations can be scoped as follows:

  • allow create - When setting new data with docRef.set() or collectionRef.add()
  • allow update - When updating data with docRef.update() or set()
  • allow delete - When deleting data with docRef.delete()
  • allow write - Applies rule to create, update, and delete.

Request vs Resource

Firestore gives us access to several special variables that can be used to compose rules.

  • request contains incoming data (including auth and time)
  • resource existing data that is being requested

This part is confusing because a resource also exists on the request to represent the incoming data on write operations. I like to use use helper functions to make this code a bit more readable.

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

function existingData() {
return resource.data
}

function incomingData() {
return request.resource.data
}

function currentUser() {
return request.auth
}

// }
// }

Functions are your best friend when writing Firestore rules. Extract any duplicated logic into a function, otherwise your rules will get messy quickly.

User Management Rules

The majority of security rules are centered around user authentication. We can get information about the current user via request.auth.

Is the user signed In?

You will probably use this one frequently. It checks to see if the user authenticated.

// allow write: if isSignedIn();

function isSignedIn() {
return request.auth != null;
}

Does the user own this document?

Certain data should only be accessed by the owner, such as their social security number, credit card details, private notes, an so on.

// match /accounts/{userId} {
// allow write: if isOwner(userId);

function isOwner(userId) {
return request.auth.uid == userId
}

Notice how the function takes a userId argument, which we can obtain from the wildcard path /accounts/{userId}. This is the best way to validate ownership of data in Firestore.

Does this user have the correct access role?

Watch my comprehensive Role-Based User Auth with Firestore lesson for an end-to-end demo.

Validating a user role is actually very easy in Firestore. In the code below, I am assuming you have a user document that contains a roles object that looks like { editor: true, admin: true }.

This document will not be available on every request, but we can read it by using the get() helper. We need to point to the user document, then interpolate the current user’s ID into the path with $(request.auth.uid). See the getUserData() function below.

function getUserData() {
return get(/databases/$(database)/documents/accounts/$(request.auth.uid)).data
}

Now we can write some functions to see if this document has an exact role, one of many roles, or all roles.

function userHasRole(role) {
return getUserData().roles[role] == true;
}

function userHasAnyRole(roles) {
return getUserData().roles.keys().hasAny(roles);
}

function userHasAllRoles(roles) {
return getUserData().roles.keys().hasAll(roles);
}

Data Validation Example

Now let’s combine some of the functions created earlier to build a robust validation rule. By chaining together rules with && we can validate the data structure of multiple fields as an AND condition. We can also use || for OR conditions.

// allow update: if isValidProduct();

function isValidProduct() {
return incomingData().price > 10 &&
incomingData().name.size() < 50 &&
incomingData().category in ['widgets', 'things'] &&
existingData().locked == false &&
getUserData().admin == true
}

Time-based Rules Examples

Firestore also includes a duration helper to generate dates that can be operated upon. For example, we might want to throttle updates to 1 minute intervals. We can create this rule by comparing the request.time to a timestamp on the document + the throttle duration.

// allow update: if isThrottled() == false;

function isThrottled() {
return request.time < resource.data.lastUpdate + duration.value(1, 'm')
}

Want to See More?

It’s impossible to cover every security rule scenario, but feel free to post your requirements in the comments below and I may add it to this article.