Firebase 3.0 Security Rules - Don't allow to push anonymous children - firebase

I'm building an application that allows users to stamp their working times. On my application are 2 types of users. One of them are the 'employee' which has only read access and the other is the 'admin' that has read and write access. The employee are allowed to read their own worktimes and the admins are allowed to create new users or add worktimes for each user/employee.
The admin fills the form and push the Data. On firebase, I checked if the user is authenticated andalso runned a validation for each property of the user object. But I'm asking myself, what if somebody copy my firebase url (which are visible in the source code) and tries to push some anonymous objects or properties to my database?
For example, I've SignUp form which requires this fields:
{
"users": {
"-KInd4V0V9k5n6yhAETd": {
"uid": "-KInd4V0V9k5n6yhAETd",
"username": "Haris",
"password": "HelloWolrd123",
"age": "21"
}
}
}
On the firebas side, I've checked with the newData method if the children are passed from the form:
".validate": "newData.hasChildren(['uid','username','password,'age'])"
I've ran already a validation for each property that checks if the data are valid like username.isString() etc.
So what if an admin pushes an user object with the developer-tools like this to my database (email property added)
{
"-KInd4V0VEEEEEEEE": {
"uid": "-KInd4V0VEEEEEEEE",
"username": "Haris",
"password": "HelloWolrd123",
"age": "21",
// anonymous Object pushed from n admin developer-console
"email": "anonymous#object.com"
}
}
This will work because all required fields are valid and the validation passed on firebase. But how can I deny that an admin can't add and pass the 'email' property to my firebase database? Because my user object structure on the Database don't have an email property. I don't wanna allow that an user can push his own anonymous objects/data to my database. Do I need to validate each property or is there a method which I can validate the Object itself? Like newData('users').isValid()? And If a property is append then isValid returns false because the object are not the same like in the database structure?

I'm not entirely sure, but think you are looking for a rule that rejects all other children. If that is indeed what you're looking for, then:
{
"rules": {
"users": {
"$uid": {
// Valid users have these properties
".validate": "newData.hasChildren(['uid','username','password,'age'])",
// The uid property must be a string and refer to an existing noder under /users
"uid": {
".validate": "newData.isString() && root.child('users').child(newData.val()).exists()"
},
// the user name must be a string
"username: {
".validate": "newData.isString()"
},
"password: {
// I really hope you're not storing a password
},
"age: {
// TODO: you're storing age as a string now, might want to fix that
".validate": "newData.isNumber()"
},
// All other properties are rejected
"$other": {
".validate": false
}
}
}
}
}

Related

Simple access control with custom claim for firebase realtime database not working [duplicate]

This question already has an answer here:
Firestore online rules simulator fails with custom claims
(1 answer)
Closed 1 year ago.
All the users for firebase project are authenticated using Phone provider.
I am setting custom claim for all users using C# as follows -
var claims = new Dictionary<string, object>()
{
{ "admin", true },
};
var pagedEnumerable = FirebaseAuth.DefaultInstance.ListUsersAsync(null);
var responses = pagedEnumerable.AsRawResponses().GetAsyncEnumerator();
while (responses.MoveNextAsync().Result)
{
ExportedUserRecords response = responses.Current;
foreach (ExportedUserRecord user in response.Users)
{
FirebaseAuth.DefaultInstance.SetCustomUserClaimsAsync(user.Uid, claims);
}
}
In the firebase realtime database I have following nodes -
{
"Configuration" : {
"Sync" : "XYZ"
},
"adminContent" : {
"key" : "val1"
}
}
I am trying to configure database access rules using custom claim as -
{
"rules": {
"adminContent": {
".read": "auth.token.admin === true",
".write": "auth.token.admin === true"
}
}
}
I am trying to use the rule playground to verify read access to adminContent node and getting error as -
The result pop up reads as -
Request -
Type read
Location /adminContent
Data null
Auth { "provider": "anonymous", "uid": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }
Admin false
Response -
READ Denied
Line 5 (/adminContent)
read: "auth.token.admin === true"
Using C# code I have verified that admin custom claim exists on the user.
Will any one pls help to fix this error?
The rules playground in the Firebase console doesn't read any profile information of existing users. If you want to test whether the read is allowed when a user has the admin claim, you'll need to specify that claim in the rules playground by selecting the Custom provider, and then editing the Auth token payload to include it.
Also see: Firestore online rules simulator fails with custom claims, which I just found and will actually close your question as a duplicate against.

Firebase Database Rules - Get displayName

I added Firebase to my website and created a signup form.
When the user clicks the "signup" button it creates a user using "Firebase Authentication", then it updates the user's displayName, and finally adds some data about the user to the database:
firebase.auth().createUserWithEmailAndPassword(email, password).then(function () {
profile = firebase.auth().currentUser;
profile.updateProfile({
displayName: "name-value"
}).then(function() {
// Update successful.
firebase.database().ref("/users/" + profile.uid).set({
data:{
country: "country-value",
city: "city-value"
}
});
});
}).catch(function(error) {
// Handle Errors here.
});
Everything worked fine until I changed the database rules from:
{
"rules": {
".read": true,
".write": true
}
}
}
To:
{
"rules": {
"users": {
".read": true,
"$user": {
".write": "auth.token.name !== null"
}
}
}
}
I used auth.token.name as it is written in the guide to check whether or not the user's displayName exists (I planned to make it check some more things about it later).
Somehow "firebase rules" 'thinks' that auth.token.name equals null, even though I set the displayName before. Why is it happening and what can I do to make it work?
Thanks in advance :)
First, Firestore has replaced Realtime Database and should be used for all projects going forward - more features.
Re Realtime DB rules, it's is best practice to check uid variable vs display name. See Variables:
{
"rules": {
".read": true,
"$comment": {
".write": "!data.exists() && newData.child('user_id').val() == auth.uid"
}
}
}
Finally, IIF you implement database security rules using Firebase Authentication, you MUST use .onAuthStateChanged to drive your application as your auth() variables WILL BE null for a second on initial page/app load as this is an asynchronous method. So, don't try and write or read anything on DOM ready. You must trigger db conversations within .onAuthStateChanged like below:
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
//USER SIGNED IN. WE ARE READY
}
else {
//USER IS NOT SIGNED IN
}
});
I had the same problem and figured out a better solution. Firebase has a feature called custom claims that is meant for the use case we have. You want to set some id on a Firebase Anonymous Auth User that is accessible from database security rules.
The high-level of the solution is to make a Cloud Function that uses the admin api to set a custom claim. You can't set custom claims from the client side.
Next, call the cloud function to update the custom claim then use Auth.auth().currentUser?.getIDTokenResult(forcingRefresh: true,... in order to retrieve the updated token which includes the custom claim. This step is critical because you need your custom claim to be sent as part of every request moving forward.
If anyone actually reads this I can type up a more detailed implementation example.

How to make a 'super admin' in Firebase?

A super admin is an user with special uid which is able to access of everyone to edit their profile and publish new content.
How to make a super admin?
Consider using custom user attributes. It is more efficient and cheaper than using Real Time Database to lookup if a user is an Admin on every authenticated request:
https://firebase.google.com/docs/auth/admin/custom-claims
You would set the Admin role on the user upon creation:
admin.auth().setCustomUserClaims(uid, {admin: true})
You can propagate it to the client after ID token refresh.
currentUser.getIdToken(true)
Then you can simply enforce the rule:
{
"rules": {
"adminContent": {
".read": "auth.token.admin === true",
".write": "auth.token.admin === true",
}
}
}
If you don't use rules or Firebase RTDB, then enforce it on your backend by parsing it from the ID token via Firebase Admin SDK:
// Verify the ID token first.
admin.auth().verifyIdToken(idToken).then((claims) => {
if (claims.admin === true) {
// Allow access to requested admin resource.
}
});
Basically, it's all about data structure and the belonging security rules.
To get started, build a data structure where you have some kind of user roles in it.
For example like this:
{
"data" : {
"user1id" : {
"name" : "MisterX"
},
"user2id" : {
"name" : "John Doe"
}
},
"users" : {
"user1id" : {
"role" : "admin"
},
"user2id" : {
"role" : "member"
}
}
}
Each user has a property called role.
Now you can define your security rules and make use of the role property to define the right access rights:
"data" : {
"$userid" : {
".read" : true,
".write" : "root.child('users').child(auth.uid).child('role') === 'admin'",
}
}
In the case above just admins are able to write to the data/userid node. You can apply this to all the different nodes you want to.

Firebase: Security rules for a collaborative app

I'm writing a note sharing app and I'm trying to find the best approach for the data structure to allow adding collaborators to user notes, while at the same time having sensible security rules for the structure in question.
What I have now is the following:
"users": {
"johndoe": {
"notes": {
"note1Key",
"note2Key"
}
},
"jane": {
"notes": {
"note3Key",
"note4Key",
"note5Key"
}
}
...
},
"notes": {
"note1Key": {
// actual note data
},
"note2Key": {
// actual note data
},
"note3Key": {
// actual note data
},
...
},
"shared": {
"johndoe" : {
"note5Key" : true,
"note3Key" : true
},
"jane" : {
"note1Key" : true
}
...
}
When "John Doe" creates a note, the note is stored in notes/noteKey with read/write access granted to owner and collaborators added by the owner. Additionally the note's key is stored in user/johndoe/notes/noteKey, which can be read and written to only by him. When this user wants to add a collaborator ("Jane") to his note, this same note key is stored in shared/jane/noteKey which can be globally read & written to. This way, when listing each user's notes, I have to read from only 2 locations to list all notes a user has access to: user/johndoe/notes and shared/johndoe.
Is there a better approach? I don't like to have the shared index globally accessible, could I somehow limit it? Since one user can potentially collaborate with a big number of different users on different notes, I'm not really sure how to set the security rules, to limit the read/write access to this index.
I was thinking about reversing the shared node logic, to store note key's under it's respectful owners sub-nodes and including a list of collaborators like so: shared/jane/noteKey/collaborators/johndoe. This way I could have a global read rule and a more restrictive write rule (each user can only write in his own shared node), however this would greatly increase the complexity of listing all notes a user has access to.
You wanted to:
allow adding owner & collaborators to user notes.
list all notes a user owned.
list all notes a user has access to.
You should have added collaborators list to each notes as follows:
{"rules":{
"users": {
"$user_id": {
"profile_and_settings":{
".write":"auth != null && auth.uid == $user_id"
},
"owned_notes":{
".write":"auth != null && auth.uid == $user_id",
"$note_id":{}
},
"accesssible_notes": {
".write":"auth != null",
"$note_id":{}
}
}
},
"notes": {
"$note_id": {
// to edit this node: must authenticated, new entry or owner of this node.
".write":"auth != null && ( !data.exists() || data.child('owner').val() == auth.uid )",
"owner":{
".validate":"newData.val() == auth.uid"
},
"collaborators":{
"$user_id":{}
},
// ... other note data
}
//...
}
}}
See related question:
Firebase rule: Do we have better ways to manage object ownership?

Security rules with dynamic paths

Say I have a database with the following structure:
Permissions
$pId (auto generated id from push)
userId
roomId
permission
User
$uId
Name
Room
$rId
Name
Is it possible to write a rule that says "allow user to modify room if there exists a permission p where p.userId = auth.uid and p.roomId = $rId and p.permission = 'admin'"?
As far as I can tell it isn't possible without nesting permission information under each room.
You'll need to change your structure a bit. Instead of using a push id to identify permissions, use a more controlled structure. For example:
"Permissions": {
"roomId": {
"userId": "role"
}
}
Now you can secure the room as you want with:
{
"rules": {
"Room": {
"$rId": {
".write": "root.child('Permissions').child($rId).child(auth.Id).val() == 'admin'"
}
}
}
}
An added advantage is that you don't have to scan all the permissions if you every want to revoke a user's permission.

Resources