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:
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 to solve the problem. Here's a common example:
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):
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.
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:
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:
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:
But authentication with uid=jill
can read from path users/jack
, and the line allowing this is highlighted as well:
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. Please feel free to leave comments below.
Top comments (0)