Switch to infinite scroll (Full book)

Firestore Security rules

We define the security rules in the Firebase console or in a firestore.rules file, referenced by firebase.json. Firebase doesn't bill reads and writes denied by security rules.

rules version

rules_version = "2"

firestore scope

We start by scoping the rules to cloud.firestore

service cloud.firestore {
    // ...
    }

database scope

We scope the rules to the current database. The security rules only affect the current database, so this can be seen as superfluous. But we use the wildcard when querying separate documents (see below).

match /databases/{database}/documents {
    // ...
}

set rules for a given collection

We target a collection. The document ID wildcard holds the requested document's ID. We can name it with what the document represents:

match /users/{user_id} { // document ID wildcard
    	// ...
}

operations and condition

allow operation, operation: if condition;

operations

read
create
update
delete

authentication, user ID

If the user is not authenticated, request.auth is null. We can filter out unauthenticated users:

allow read: if request.auth != null;

The user's authentication uid (if logged in) is available as request.auth.uid:

request.auth.uid

Note: if auth is null, trying to read uid triggers a failsafe mechanism that denies the request.

authorize specific documents

Document ID based authorization: We authorize the operation if the document's ID matches some condition:

    match /players/{player_id} {
         allow read: if request.auth.uid == player_id;
    }

Field value based authorization: we authorize the operation based on the value of a specific field. Either from the requested document or from the uploaded document.

  • resource.data represents the requested document.
  • request.resource.data represents the uploaded document
  • For example, we check the requested document's owner property against auth.uid. In this case, we ignore the uploaded document's data which can be tampered with.
    match /planets/{planet_id} {
         allow read: if request.auth.uid == resource.data.owner.id;
    }

If the document is missing the field, the request is denied.

authorization based on separate documents

We read a document with get(). It is a billed read.

first example (do not use)

This unlocks a pattern where we read authorization data in a separate document, such as in the user document, which would store the user's entitlements:

get(/databases/$(database) / documents / users / $(request.auth.uid)).data.rank

For example, we read the user's rank in the database to authorize a write on any character:

    match /characters/{character_id} {
         allow update: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.rank == "Game Master";
    }

Note: We should use Firebase Auth custom claims instead:

request.auth.token.rank == "Game Master";

second example

For example, we read the player's character's zone to determine if it can read the requested overworld character:

match /overworld_characters/{overworld_character} {
     allow read: if get(/databases/$(database)/documents/characters/$(request.auth.uid)).data.zone == resource.data.zone;
}

Note:

  • if a query matches 10 documents, the get() is run only once, so it only triggers a single read
  • if a query, by its nature, could match at least one document that is not authorized by the security rule, the entire query is rejected. This forces the client to query only authorized documents. In the example, it forces the client to use a where() clause instead of fetching all overworld_characters. That is, we cannot rely on security rules to filter documents out of a broad query.

payload validation

request.resource.data is the request's payload. We can validate critical fields:

	// simple check
request.resource.data.age >= 0

  // check against auth uid.
request.auth.uid == request.resource.data.uid;

  // check both requested document and uploaded document
  allow update,delete: if
  request.auth.uid == resource.data.uid
  &&
  request.auth.uid == request.resource.data.uid;

Alternative: We can perform validation in Cloud Functions and forbid writes coming from the client.

earlymorning logo

Firestore Security rules

We define the security rules in the Firebase console or in a firestore.rules file, referenced by firebase.json. Firebase doesn't bill reads and writes denied by security rules.

rules version

rules_version = "2"

firestore scope

We start by scoping the rules to cloud.firestore

service cloud.firestore {
    // ...
    }

database scope

We scope the rules to the current database. The security rules only affect the current database, so this can be seen as superfluous. But we use the wildcard when querying separate documents (see below).

match /databases/{database}/documents {
    // ...
}

set rules for a given collection

We target a collection. The document ID wildcard holds the requested document's ID. We can name it with what the document represents:

match /users/{user_id} { // document ID wildcard
    	// ...
}

operations and condition

allow operation, operation: if condition;

operations

read
create
update
delete

authentication, user ID

If the user is not authenticated, request.auth is null. We can filter out unauthenticated users:

allow read: if request.auth != null;

The user's authentication uid (if logged in) is available as request.auth.uid:

request.auth.uid

Note: if auth is null, trying to read uid triggers a failsafe mechanism that denies the request.

authorize specific documents

Document ID based authorization: We authorize the operation if the document's ID matches some condition:

    match /players/{player_id} {
         allow read: if request.auth.uid == player_id;
    }

Field value based authorization: we authorize the operation based on the value of a specific field. Either from the requested document or from the uploaded document.

  • resource.data represents the requested document.
  • request.resource.data represents the uploaded document
  • For example, we check the requested document's owner property against auth.uid. In this case, we ignore the uploaded document's data which can be tampered with.
    match /planets/{planet_id} {
         allow read: if request.auth.uid == resource.data.owner.id;
    }

If the document is missing the field, the request is denied.

authorization based on separate documents

We read a document with get(). It is a billed read.

first example (do not use)

This unlocks a pattern where we read authorization data in a separate document, such as in the user document, which would store the user's entitlements:

get(/databases/$(database) / documents / users / $(request.auth.uid)).data.rank

For example, we read the user's rank in the database to authorize a write on any character:

    match /characters/{character_id} {
         allow update: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.rank == "Game Master";
    }

Note: We should use Firebase Auth custom claims instead:

request.auth.token.rank == "Game Master";

second example

For example, we read the player's character's zone to determine if it can read the requested overworld character:

match /overworld_characters/{overworld_character} {
     allow read: if get(/databases/$(database)/documents/characters/$(request.auth.uid)).data.zone == resource.data.zone;
}

Note:

  • if a query matches 10 documents, the get() is run only once, so it only triggers a single read
  • if a query, by its nature, could match at least one document that is not authorized by the security rule, the entire query is rejected. This forces the client to query only authorized documents. In the example, it forces the client to use a where() clause instead of fetching all overworld_characters. That is, we cannot rely on security rules to filter documents out of a broad query.

payload validation

request.resource.data is the request's payload. We can validate critical fields:

	// simple check
request.resource.data.age >= 0

  // check against auth uid.
request.auth.uid == request.resource.data.uid;

  // check both requested document and uploaded document
  allow update,delete: if
  request.auth.uid == resource.data.uid
  &&
  request.auth.uid == request.resource.data.uid;

Alternative: We can perform validation in Cloud Functions and forbid writes coming from the client.