I have a firebase realtime database. Currently it contains two nodes admin and common
{
"admin" : {
"adminval" : 9898574632,
"adminval1" : 645354536,
"adminval2" : 7776756433
},
"common" : {
"commonval" : 123433221
}
}
I added to each user custom claim roles which describes roles user has in my system. It looks like this
{'roles': ['ROLE_ADMIN', 'ROLE_USER']}
Now I would like to restrict access so only users with claim ROLE_ADMIN are allowed to read/write admin node and with either of the roles can read/write node common.
How to do it ? I tried something like it:
{
"rules": {
"admin": {
".read": "auth.token.roles.contains('ROLE_ADMIN')",
".write": "auth.token.roles.contains('ROLE_ADMIN')"
}
"common": {
".read": "auth.token.roles.contains('ROLE_USER') || auth.token.rules.contains('ROLE_ADMIN')",
".write": "auth.token.roles.contains('ROLE_USER') || auth.token.rules.contains('ROLE_ADMIN')"
}
}
}
I am afraid we cannot use Array or List in Realtime database security rules. Language of rules can work with only limited set of types. You can check the documentation here https://firebase.google.com/docs/reference/security/database.
We can walk around this limitation for instance by using String in claims:
{'roles': 'ROLE_ADMIN,ROLE_USER'}
Now we can check the appropriate role using
".read": "auth.token.roles.contains('ROLE_USER') || auth.token.rules.contains('ROLE_ADMIN')"
, because String supports contains method. Another option, as you suggest in your comment, is to set claims like
{'role_admin': true, 'role_user': true}
and then check the role in rules
".read": "auth.token.role_admin == true || auth.token.role_user == true"
Side note: we can use List and in operator in Firestore security rules, see doc here https://firebase.google.com/docs/reference/rules/rules.List.
I've never used arrays in custom claims, so am not sure how that works, or why it doesn't work for you.
But I did notice that you're doing a weird mix of role-based access control and level-based access control in your rules, so want to give some general hints/observations:
You're marking your admins with both roles: ROLE_ADMIN and ROLE_USER.
But then you have an OR in the common node, checking for either role. This means there's no need to give the admin the ROLE_USER too.
Keep in mind that custom claims are limited to 1000 bytes and are sent with every request. I'd highly recommend dropping the ROLE_ prefix, since that's already implied in the roles name of the claim.
If you have a universal logic that admins can see more than regular users, consider using a numeric access_level claim instead. Say a regular user is level: 1, while an admin is level: 2. You can then in your claims check for the minimum level: ".read": "auth.token.access_level >= 1"
Related
I recently received an email from firebase telling me that my realtime database has insecure rules. These are the rules that I have set:
{
"rules": {
".read": "auth != null",
".write": "auth != null"
}
}
Is this not a secure rule?
Email/Password is the only sign-in method that I have enabled.
firebaser here
I'm sorry if the email wasn't very explicit about what isn't secure about those rules. Securing your user's data is a crucial step for any app that you make available, so I'll try to explain a bit more about how that works below.
The (default) rules you have allow anyone who is signed in to your back-end full read/write access to the entire database. This is only a very basic layer of security.
On the one hand this is more secure than just granting everyone access to your database, at least they have to be signed in.
On the other hand, if you enable any auth provider in Firebase Authentication, anyone can sign in to your back-end, even without using your app. Depending on the provider, this can be as easy as running a bit of JavaScript in your browser's developer console. And once they are signed in, they can read and write anything in your database. This means they can delete all data with a simple command like firebase.database().ref().delete().
To make the data access more secure, you'll want to more tightly control what each signed-in user can do. For example, say that you keep a profile with information about each user under /users. You might want to allow all users to access these profiles, but you definitely want users to only be allowed to modify their own data. You can secure this with these rules:
{
"rules": {
"users": {
".read": true,
"$user_id": {
// grants write access to the owner of this user account
// whose uid must exactly match the key ($user_id)
".write": "$user_id === auth.uid"
}
}
}
}
With these rules, everyone (even non-authenticated users) can read all profiles. But each profile can only be modified by the user whose profile it is. For more on this, see the Firebase documentation on securing user data.
In addition to ensuring that all access to data is authorized, you'll also want to ensure that all data stored is valid to whatever rules you have for you app. For example, say that you want to store two properties for a user: their name, and their age (just for the sake of the example, in reality you'd probably store their date-of-birth instead). So you could store this as something like:
"users": {
"uidOfPuf": {
"name": "Frank van Puffelen",
"age": 48
}
}
To ensure only this data can be written, you can use this rules:
{
"rules": {
"users": {
".read": true,
"$user_id": {
".write": "$user_id === auth.uid",
".validate": "data.hasChildren('name', 'age')",
"name": {
".validate": "data.isString()",
},
"age: {
".validate": "data.isNumber()",
},
"$other: {
".validate": false
}
}
}
}
}
These rules ensure that each user profile has a name and age property with a string and numeric value respectively. If someone tries to write any additional properties, the write is rejected.
Above is a quick primer on how to think about securing your (user's) data. I recommend that you check out the Firebase security documentation (and the embedded video) for more.
Update: since May 2021 you can also use Firebase App Check to restrict access to calls just coming from your web site or app. This is another, quick way to reduce the abuse of your database. This approach is not foolproof though, so you'll want to combine App Check for broad protected, with the security rules for fine-grained control.
You can also mute alerts by visiting the link at the bottom of the email.
https://console.firebase.google.com/subscriptions/project/<YOUR_PROJECT_NAME>
I changed rules
{
"rules": {
"users": {
".read": true,
"$user_id": {
// grants write access to the owner of this user account
// whose uid must exactly match the key ($user_id)
".write": "$user_id === auth.uid"
}
}
}
}
But after that page not working.
my current firebase realtime security rules are like below
{
"rules": {
"users": {
".read" : true,
".indexOn": ["email"],
"$user_id": {
".read": true,
".write": "auth != null && $user_id === auth.uid"
}
}
}
}
they translates as only the authenticated user can write the data to his own node under users/
However, we have admin users who should be able to modify the data of non admin users.
The way we identify admin users are a user property isAdmin which is true for admin users. so the sample data with a admin and non admin user looks like below
{
"users": {
"kldjjfjf" : {
"name": "vik", "isAdmin": true
},
"lfllfomr": {
"name": "neeti", "isAdmin": false
}
}
Please advise what is the best practice to handle this kind of usecases? doing a .write true will solve it but then it will make it open to anyone to modify anyone's data.
The simplest ways I've found to allow Administrative access is to:
Use a custom claim for admins
Use a whitelist of admin UIDs
Use a custom claim for admins
You can add custom claims to Firebase Authentication user profiles with the Admin SDK. Claims are custom key/value pairs that you determine the meaning of yourself. The first example from the documentation shows setting a claim called admin to true, for example with the Admin SDK for Node.js:
admin.auth().setCustomUserClaims(uid, {admin: true}).then(() => {
// The new custom claims will propagate to the user's ID token the
// next time a new one is issued.
});
Once a custom claim is set, it is transported to the client when it signs in, and is also available in security rules. You can check the above with:
".write": "auth != null && ($user_id === auth.uid || auth.token.admin === true)"
Use a whitelist of admin UIDs
A simple alternative is to store a list of UIDs in your database of users with specific privileges. For example, you could have a top-level Admins list:
Admins
uidOfVik: true
uidOfPuf: true
The above means that you and me are admins. You then check for those in the security rules:
".write": "auth != null && ($user_id === auth.uid || root.child('Admins').child(auth.uid).exists())"
Here's an alternative:
Firebase security rules only apply to clients connecting normally to the application.
If you have your own back end (I can't assume that, because Firebase is made for Serverless computing) then it can connect to the application with the admin SDK, which bypasses the security rules.
Alternatively, you can make a separate application for your admin users, that will connect to firebase using the admin SDK.
More information: Firebase Documentation - Firebase Admin SDK
Sorry if the title was confusing, I'll do a better job explaining here.
I was reading up on the Firebase security documentation and came across this page: https://firebase.google.com/docs/database/security/quickstart?authuser=0. Specifically I was looking at the "Sample Rules" -> "User" section which contained the following code:
// These rules grant access to a node matching the authenticated
// user's ID from the Firebase auth token
{
"rules": {
"users": {
"$uid": {
".read": "$uid === auth.uid",
".write": "$uid === auth.uid"
}
}
}
}
Assuming I understand this correctly, you will only be able to read YOUR data under the "users" node, and nobody else's.
So in my app I need to retrieve some data about a "friend" (friend count, username, posts they've made, etc) which is stored under my "friend's" user id in the "users" node. So it looks like this:
{
"users": {
"friend_uid": {
"friend_count": 10,
"username": "john",
"posts": [
"message": "post1"
],
"age": 18,
...
}
}
}
How can I do this based on the permissions above? If this is such a common task (getting some basic information about your friend), then why do a lot of tutorials show that you should set the rules to what I pasted above?
How do developers get the data they need? Do they have 2 nodes, "users" and "private_users", where in "users" they store all of the public information for retrieval from anyone, and "private_users" is where they store all the private info about the user and set stricter read/write rules?
Or do they set database rules on the individual attributes like age, name, etc?
You've basically given the answer yourself... You could either separate the data nodes and add an object with public data and one with private data, where both would have different rules for access.
For the public data node of a user, you of course wouldn't set the rules to allow complete public access when reading, but also limit it to users that are in the friend list.
Another option is to set rules on the individual properties, but this makes the rules quite big and harder to maintain, as every additional parameter needs to be added to the rules manually.
An additional thing I see when looking at your data is the missing data denormalisation. Each user object contains a list of messages/posts done by that user. So if you query a user in users/UID, you'd fetch the whole object including the possibly millions of posts/messages. So you should definitely exclude those from the user objects.
I have a client that would like to be able to make a list of restricted emails that can access the data. So anyone else coming to the app can't read/write any data at all ( ideally can't even log in but I don't think that's possible with Firebase? ). Any ideas on how to go about this? I had thought of having an array of accepted emails and checking whether their email existed in the security rules but that didn't seem to work. I had the following in the database:
"validEmails": ["test#test.com"]
and then in the security rules:
".read": "root.child('validEmails').val().indexOf(auth.token.email) > -1"
But it looks like you can't use indexOf in those security rules.
Maybe I need to have a list of acceptable emails, and then when a user signs up it checks whether they're in that list and adds their UID to an accepted list? I guess I could do this through a cloud function or something?
Any help would be much appreciated.
Cheers
Have the list of allowed user's emails in the database:
"whitelist": {
"fred#gmail%2Ecom": true,
"barney#aol%2Ecom": true
}
Since periods are not allowed in keys, you need to escape strings with periods before storing them.
Then in the database rules:
{
"rules": {
"whitelist": {
".read": false,
".write": false
},
".read": "root.child('whitelist').child(auth.token.email.replace('.', '%2E')).exists()",
".write": "root.child('whitelist').child(auth.token.email.replace('.', '%2E')).exists()"
}
}
User's email is accessible via auth.token.email. You need to escape the dots (. -> %2E) and check if the key exists on the whitelist.
These rules don't allow anyone read or write access to the /whitelist section of the database. Modification is only possible via firebase console.
Thanks guys, what I ended up doing was having a list of acceptable emails:
{
"validEmails": ["test#test.com"],
"validUsers": {}
}
and then have a cloud function run to check when a user signed up if their email was in the valid email list. If it was then it added them to the valid users list and if not it deleted the newly created user. I also set up data rules so that only users within validUsers could access the data.
The front-end then handled the redirection etc for invalid users.
Once you enable the authentication module of Firebase I believe you can't restrict it to email addresses or domains. However you could secure your database another way. If your users are already registered and you know their uid, then you can restrict read and write access based on these.
Lets pretend you have an acl object in the database, you can list the users and their uid with their read/write permissions.
These rules will check each request and only allow authorised users to access the data.
{
"acl": {
[
{
"uid: "abc123"
"canRead": true,
"canWrite": true
},
{
"uid": "def456",
"canRead": true,
"canWrite": false
}
},
"secure": {
".read": { root.child('acl').child(auth.uid).child('canRead').val() == true }
".write": { root.child('acl').child(auth.uid).child('canWrite').val() == true }
}
}
I’d like a security rule that lets anyone get a list of users and read their names, but only allows logged in users to view their own email.
Here’s an example data structure:
"User" : {
"abc123" : {
"name" : "Bob",
"email" : "bob#hotmail.com"
}
}
A naive approach to a security rule might be to do the following:
"User" : {
"$user" : {
"name" : {
".read" : true
},
"email" : {
".read” : "auth.uid === $user"
}
}
}
However because there is no read rule at the User level, requests to read the list will be denied. But adding a read rule at the User level will override the email rule, and make every child node readable (see Rules Cascade in Firebase's Security Guide).
The Security guide does point out that Rules Are Not Filters, but doesn’t offer much guidance as to what to do about it.
Should I just split my User entity up into PrivateUser and PublicUser?
To let anyone get a list of users and read their names. AND to allow logged in users to view their own email.
Zac says: first think about access, and then to model objects that are either completely public or completely private.
Type 1:
{"rules":{
"user_publicly":{"$user:{
".read":true,
"name":{}
}},
"user_privately":{"$user:{
".read":"auth != null && $user == auth.uid",
"email":{}
}}
}}
Type 2:
{"rules":{
"user":{"$user:{
"public":{
".read":true,
"name":{}
},
"private":{
".read":"auth != null && $user == auth.uid",
"email":{}
}
}}
}}
A "workaround" would be to use Firestore (has a lot of the good things from Firebase Realtime Database, but adds more querying options, etc).
There is no "rules are not filters" restriction in Firestore!
EDIT:
Thanks to #DougStevenson for making me be more specific.
In Firestore, rules are still not filters, but they are compatible with filtering, unlike in Firebase Realtime DB.
Though, you have to construct your query in such a way, as to only return objects for which you have read permission (otherwise you get a security exception).
Here are some starting point docs:
Security rules:
https://firebase.google.com/docs/firestore/reference/security/
Queries: https://firebase.google.com/docs/firestore/query-data/queries