Firebase Realtime Database security rules are how you secure your data from unauthorised users and protect your data structure.
In this quick tip tutorial, I will explain how to configure your database security rules properly so that only authorised users have read or write access to data. I'll also show you how to structure your data to make it easy to secure.
The Problem
Let's assume we have JSON data in our Firebase database, as in the example below:
{ "users" : { "user1" : { "firstName" : "Chike", "lastName" : "Mgbemena", "age": "89" "phoneNumber" : "07012345678" }, "user2" : { "firstName" : "Godswill", "lastName" : "Okwara", "age": "12" "phoneNumber" : "0701234" }, "user3" : { "firstName" : "Onu", "lastName" : 543, "age": 90 "phoneNumber" : "07012345678" }, ... } }
Looking at the database, you can see that there are some issues with our data:
- Two users (
user1
anduser3
) have the same phone numbers. We'd like these to be unique.
-
user3
has a number for last name, instead of a string. -
user2
has only seven digits in their phone number, instead of 11. - The age value for
user1
anduser2
is a string, while that ofuser3
is a number.
With all these flaws highlighted in our data, we have lost data integrity. In the following steps, I will show you how to prevent these from occurring.
Permissive Rules
The Firebase realtime database has the following rule types:
Type | Function |
---|---|
.read |
Describes if and when data is allowed to be read by users. |
.write |
Describe if and when data is allowed to be written. |
.validate |
Defines what a correctly formatted value will look like, whether it has child attributes, and the data type. |
.indexOn |
Specifies a child to index to support ordering and querying. |
Read more about them in the Firebase docs.
Here is a very permissive rule for the users
key in our database.
{ "rules": { "users": { // users is readable by anyone ".read": true, // users is writable by anyone ".write": true } } }
This is bad, because it gives anyone the ability to read or write data to the database. Anyone can access the path /users/
as well as deeper paths. Not only that, but no structure is imposed on the users' data.
Access Control Rules
{ "rules": { "users": { "$uid": { ".read": "auth.uid == $uid", ".write": "auth.uid == $uid", } } } }
With these rules, we control access to the user records to logged-in users. Not only that, but users can only read or write their own data. We do this with a wildcard: $uid
. This is a variable that represents the child key (variable names start with $
). For example, accessing the path /users/user1
, $uid
is "user1"
.
Next, we make use of the auth
variable, which represents the currently authenticated user. This is a predefined server variable supplied by Firebase. In lines 5 and 6, we're enforcing an accessibility constraint that only the authenticated user with the same id as the user record can read or write its data. In other words, for each user, read and write access is granted to /users/<uid>/
, where <uid>
represents the currently authenticated user id.
Other Firebase server variables are:
now |
The current time in milliseconds since Linux epoch. |
root |
A RuleDataSnapshot representing the root path in the Firebase database as it exists before the attempted operation. |
newData |
A RuleDataSnapshot representing the data as it would exist after the attempted operation. It includes the new data being written and existing data. |
data |
A RuleDataSnapshot representing the data as it existed before the attempted operation. |
auth |
Represents an authenticated user's token payload. |
Read more about these and other server variables in the Firebase docs.
Enforcing Data Structure
We can also use Firebase rules to enforce constraints on the data in our database.
For example, in the follow rules, in lines 8 and 11, we are ensuring rules that any new value for the first name and last name must be a string. In line 14, we make sure that age is a number. Finally, in lines 17 and 18, we're enforcing that the phone number value must be a string and of length 11.
{ "rules": { "users": { "$uid": { ".read": "auth.uid == $uid", ".write": "auth.uid == $uid", "firstName": { ".validate": "newData.isString()" }, "lastName": { ".validate": "newData.isString()" }, "age": { ".validate": "newData.isNumber()" }, "phoneNumber": { ".validate": "newData.isString() && newData.val().length == 11" }, } } } }
But how do we prevent duplicate phone numbers?
Preventing Duplicates
Next, I'll show you how to prevent duplicate phone numbers.
Step 1: Normalize the Data Structure
The first thing we need to do is to modifying the root path to include a top-level /phoneNumbers/
node. So, when creating a new user, we will also add the user's phone number to this node when validation is successful. Our new data structure will look like the following:
{ "users" : { "user1" : { "firstName" : "Chike", "lastName" : "Mgbemena", "age": 89, "phoneNumber" : "07012345678" }, "user2" : { "firstName" : "Godswill", "lastName" : "Okwara", "age": 12, "phoneNumber" : "06034345453" }, "user3" : { "firstName" : "Onu", "lastName" : "Emeka", "age": 90, "phoneNumber" : "09034564543" }, ... }, "phoneNumbers" : { "07012345678": "user1", "06034345453": "user2", "09034564543": "user3", ... } }
Step 2: Enforce New Data Structure
We need to modify the security rules to enforce the data structure:
{ "rules": { "users": { "$uid": { ... "phoneNumber": { ".validate": "newData.isString() && newData.val().length == 11 && !root.child('phoneNumbers').child(newData.val()).exists()" }, } } } }
Here, we're making sure the phone number is unique by checking if it is already a child of the /phoneNumbers/
node with the given phone number as key. In other words, we're checking that the phone number has not been registered by a user already. If it has not, then validation is successful and the write operation will be accepted—otherwise it will be rejected.
Your app will need to add the phone number to the phone numbers list when creating a new user, and it will need to delete a user's phone number if that user is deleted.
Simulating Validation and Security Rules
You can simulate your security rules in the Firebase console by clicking the Simulator button. Add your security rules, select the type of simulation (either read or write), input some data with a path, and click the Run button:
If the value of the first name is a number instead of a string, validation will fail and write access is denied:
Conclusion
In this quick tip tutorial, you learned about Firebase Database security rules: how to prevent unauthorised access to data and how to make sure that data in the database are structured.
To learn more about Firebase Database security rules, refer to the official documentation. And check out some of our other Firebase tutorials and courses here on Envato Tuts+!
Comments