Say you’re using Firebase with Cloud Firestore to handle user login and registration for a React Native app. You have the following handler for a user registration button: (Credit: this freeCodeCamp tutorial).

const onRegisterPress = () => {
        if (password !== confirmPassword) {
            alert("Passwords don't match.")
            return
        }
        firebase
            .auth()
            .createUserWithEmailAndPassword(email, password)
            .then((response) => {
                const uid = response.user.uid
                const data = {
                    id: uid,
                    email,
                    fullName,
                };
                const usersRef = firebase.firestore().collection('users')
                usersRef
                    .doc(uid)
                    .set(data)
                    .then(() => {
                        navigation.navigate('Home', {user: data})
                    })
                    .catch((error) => {
                        alert(error)
                    });
            })
            .catch((error) => {
                alert(error)
        });
    }

The firebase.auth().createUserWithEmailAndPassword() method is called to perform the actual user creation. After hitting the button you can see your new user being added to the Firebase console:

Alt Text

But what if you hit the following error?

FirebaseError: [code=permission-denied]: Missing or insufficient permissions

Many top-voted answers on StackOverflow recommend setting unsafe rules, like this:

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

This is bad. allow read, write: if true; does exactly what it says: It allows everyone (yes, everyone on the internet) to read and write to any document in your Firebase store. This is not appropriate for production.

Despite these warnings, such answers still float to the top of every StackOverflow thread on the topic, for the simple reason that it “solves” the problem for “testing purposes”. But what happens after “testing”?

I found the mess of answers and the official Firebase documentation somewhat confusing to wade through. I hope the following will help.

Where to set rules?

It wasn’t obvious to me. They are here (ensure you are in Cloud Firestore and not Realtime Database):

Alt Text

Let’s look at some of the suggested solutions from StackOverflow or the Firebase documentation and see what each is actually doing:

Default: allow open access for 30 days

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if
          request.time < timestamp.date(2020, 9, 16);
    }
  }
}

This is the default rule set that you are given when you set up your Firebase project and add a Cloud Firebase database: it allows open access to everyone for 30 days, and then will deny access to everyone.

Alt Text

Some answers suggest simply pushing this date forward. This is clearly as bad as setting allow read, write: true. This is not a permanent solution.

Allow read/write by any authenticated user

Another common suggestion is this:

// Allow read/write access on all documents to any user signed in to the application
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if request.auth != null;
    }
  }
}

Better, if you are comfortable with any authenticated user being able to read and write to anything. However, I am making a registration handler - which means anyone can make an account and become an authenticated user. Let’s keep looking.

Content-owner only access

Firebase documentation then suggests this for content-owner only access:

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow only authenticated content owners access
    match /some_collection/{userId}/{documents=**} {
      allow read, write: if request.auth != null && request.auth.uid == userId
    }
  }
}

Seems perfect, right? Except this literal rule won’t work for my registration handler either. A mindless copy-paste won’t do here: some_collection does not exist. In fact, in a new Firebase Cloud Firestore, no collections exist:

Alt Text

If you recall from the handler above, the then() callback accesses a Firestore collection called users:

const usersRef = firebase.firestore().collection('users')
                usersRef
                    .doc(uid)
                    .set(data)

So the final non-obvious step is to ensure that your rule and the firebase.firestore().collection() call are actually referencing the same collection.

The collection doesn’t need to exist; you just need a rule matching it

There is no need to create an empty users collection ahead of time. The firebase.firestore().collection('users').doc(uid).set(data) call simply has to find a matching ruleset. In this case, the match is /users/{userId}/{documents=**}.

If the users collection does not exist, it will be created.

Note that a typo (collection('Users'), collection('user')) would result in a permission error - not because the collection doesn’t already exist, but because there is no matching ruleset to allow the write.

You can separate read and write rules

And finally, read and write rules can be separated into their own conditions. For example, the following will allow any authenticated user to read data for any document in the users collection. But they can only write to (create/update/delete) their own:

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow only authenticated content owners access
    match /users/{userId}/{documents=**} {
      allow write: if request.auth != null && request.auth.uid == userId;
      allow read: if request.auth != null;
    }
  }
}

Lastly, I recommend looking at your newly created documents to understand their structure:

Alt Text

Use the Rules Playground to test rules

Note in this example, authentication with uid=jill cannot write to the path users/jack. The line responsible for the write deny is highlighted:

Alt Text

Alt Text

But authentication with uid=jill can read from path users/jack, and the line allowing this is highlighted as well:

Alt Text

No more nuclear option

I hope this helped clarify use of Cloud Firestore rules, and allows you to steer away from the unnecessarily broad allow read, write: if true; option.

This post is also published at dev.to