Firebase Security Rules

Tools used:
Backend: Firebase -- Version: 7.7.0
Frontend: @ionic/angular -- Version: 4.7.1

We are preparing our app to go public, so the first thing we want to do is to update our security rules on the server, we do not want people connecting to the app and having access to someone else’s data.

Firestore Security

With the Cloud Firestore Security Rules, we can focus on building a great user experience, without having to manage infrastructure or write server-side authentication and authorization code.

The idea is to authenticate users through Firebase Authentication and set up rules to determine who has access to data stored in Cloud Firestore.

We can find our security rules in the Rules tab in the Cloud Firestore section of the Firebase Console.

To start securing our database we need to understand how the security rules work, let’s take a look at the default ones that come when we create the app.

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

The security rules work matching documents in the database, they have two permissions, read and write which are both false by default, meaning, no one has access to the database.

To start working with them, we tell them to allow all read/write operations, since we’re going to be in development mode:

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

The =** symbol is a cascade operator, rules don’t cascade by default.

So if we set up a read/write rule for the document users/{userId} but don’t set up read/write rules for users/{userId}/tasks/{taskId} no one will have access to the taskId documents.

When we use the =** operator, we’re telling Firestore rules that if the user matches the condition to read that document, they should be able to read all the sub-collections and documents below that tree.

The brackets mean we’re using a wild-card, for example, if I have a collection called users that has the documents for each user’s profile, I’d only want the profile owner to be able to have read/write access.

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

Notice how we’re getting the user’s uid from the request object. When working with Firestore rules, we have two available objects.

The request object has information about the request made, such as the authenticated user: request.auth, and the time the request was made: request.time.

The resource object is THE Firestore document we’re accessing. For example, let’s say we have public and private profiles, each profile has a flag called public and it’s either set to true or false.

The resource object gives us access to that flag:

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

    match /myCollection/myDocument {
      allow read: if resource.data.public == true;
    }
  }
}

In that case, people will only be able to read profiles marked as public.

Storage Security

We should also set up rules for Firebase Storage, that way we can protect our users’ files.

We will need to go to: console.firebase.google.com/project/YOURAPPGOESHERE/storage/rules

Identifying our user is only part of security. Once we know who they are, we need a way to control their access to files in Cloud Storage.

Cloud Storage lets we specify per file and per path authorization rules that live on our servers and determine access to the files in our app. For example, the default Storage Security Rules require Firebase Authentication to perform any read or write operations on all data:

service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow read, write: if request.auth != null;
    }
  }
}

Data Validation

Firebase Security Rules for Cloud Storage can also be used for data validation, including validating file name and path as well as file metadata properties such as contentType and size:

service firebase.storage {
  match /b/{bucket}/o {
    match /images/{imageId} {
      // Only allow uploads of any image file that's less than 5MB
      allow write: if request.resource.size < 5 * 1024 * 1024
                   && request.resource.contentType.matches('image/.*');
    }
  }
}