Environment: Firebase Unity 6.6.0, Unity Editor 2019.2.8f1
I am seeing the error "This client does not have permission to perform this operation." for new Anonymous users who authenticated moments before. If I wait 5-30 seconds, and connect again with the same user, the issue does not appear.
My relevant Real Time Database security rules
{
"rules": {
"gameSettings":{
".read": true,
".write": false
},
"userCompletedMissions": {
".write": false,
"$uid": {
".read": "$uid === auth.uid",
}
},
"userRanks": {
".write": false,
".read": "auth != null",
"english": {
".indexOn": ["rankOrderKey", "dRankOrderKey", "wRankOrderKey", "mRankOrderKey"],
},
"spanish": {
".indexOn": ["rankOrderKey", "dRankOrderKey", "wRankOrderKey", "mRankOrderKey"],
}
}
}
}
My firebase project is a mobile game. First time players create an Anonymous FirebaseUser account with await auth.SignInAnonymouslyAsync();, and immediately listen for data. Anything that requires ".read": true works flawlessly, but anything with ".read": "auth != null" and ".read": "$uid === auth.uid" trips my Security Rules and throws the error "This client does not have permission to perform this operation."
For example: FirebaseDatabase.DefaultInstance.GetReference("userRanks/english/SOME_USER_ID").ValueChanged += SomeFunction(...);
It's clear that the User is authenticated but Firebase's Security Checker doesn't know yet. Is there a way for me to wait until I am actually Authenticated?
Edit: additional information
What I did find interesting was the logs from following these repro steps:
1. My Unity Game had an authenticated Firebase User "Semadikp6FheE1FZMqi7MnCy0Iv2" in the Unity Editor on my Mac OS Desktop
2. In the Firebase Console, I navigated to the Authentication Tab, found the user "Semadikp6FheE1FZMqi7MnCy0Iv2", and deleted the account.
3. The next time I started my Unity Editor, all of the "This client does not have permission to perform this operation" errors referenced the deleted user "Semadikp6FheE1FZMqi7MnCy0Iv2"
4. My Unity game tolerates runtime errors by reloading the game scene (Unity Editor Play Mode does not stop). When the scene reloaded, it initialized everything from scratch again including Firebase API calls. The Firebase User it referenced on this attempt was a new Firebase User "UTc2NqwgYPTUFnnDCGSTLQm0yv63". Everything worked smoothly.
These seems like a bug related to caching, either on the Unity Firebase client, or Firebase service.
Further explanation:
I was repeatedly deleting the Anonymous account connected to my Unity Editor device in an attempt to reproduce the bug preventing my team mate from running the game. Whenever he runs the game in Unity Editor, he gets hit with "This client does not have permission to perform this operation" errors that do not clear up on subsequent attempts. He has joined this project a couple weeks ago, and the project is 11 months old. It's very likely an innocent issue like a "file is missing" from the GIT checkout. Unfortunately, I haven't found any obvious mistakes on my part, and the problem remains unsolved. Please advise.
It doesn't sound like this is an issue based on the question, but the easiest suggestion I have is to make sure you have anonymous authentication enabled:
Next, you can check to see if the user is currently authenticated by checking:
FirebaseAuth.DefaultInstance.CurrentUser != null;
If you wanted, you could also check IsAnonymous on CurrentUser. I like to check this in a handler to the StateChanged listener for a global "the user has signed in" signal.
With all that said, since you're using await, you can always say:
FirebaseUser user = FirebaseAuth.DefaultInstance.SignInAnonymouslyAsync();
if (user != null) {}
or in a coroutine (my favorite when working with Unity):
Task<FirebaseUser> signInTask = FirebaseAuth.DefaultInstance.SignInAnonymouslyAsync();
yield return new WaitUntil(() => signInTask.IsComplete);
FirebaseUser user = signInTask.Result;
if (user != null) {}
Finally, I noticed that you're using Firebase 6.4.0 rather than 6.6.0 (the current latest). At some point there was an issue with FirebaseAuth.DefaultInstance going out of scope if you didn't hold onto it (that is, if the GC decided to run through after you let go of any reference to FirebaseAuth.DefaultInstance), calls to other Firebase products wouldn't have authentication information.
I would recommend updating to the latest SDK if you can, but also try to hold onto FirebaseAuth in a script marked as DontDestroyOnLoad. Kind of like this:
public class AuthHolder: MonoBehaviour {
public FirebaseAuth Auth {get; private set;}
void Start() {
Auth = FirebaseAuth.DefaultInstance;
DontDestroyOnLoad(gameObject); // should be at the root of a scene
}
void OnDestroy() {
Auth = null; // make sure it can get cleaned up when you exit the game
}
}
If this fixes your issue on the latest Firebase Unity SDK, open up an issue on the GitHub Issues page.
If you haven't seen it yet, I do cover Authentication and Unity in some detail in this video. Although it focuses on Email/Password rather than Anonymous authentication.
I hope that helps!
--Patrick
Related
I have the following Firebase realtime database rules:
{
"rules": {
".read" : "auth != null",
".write": false,
"Games": {
".indexOn" : "WeekId"
},
"Version": {
".read" : true
}
...
}
I keep getting the following email from Google:
[Firebase] Your Realtime Database 'xxx-9e542' has insecure rules`
We've detected the following issue(s) with your security rules:
any logged-in user can read your entire database
Without strong security rules, anyone who has the address of your database can read / write to it, leaving your data vulnerable to attackers stealing, modifying, or deleting data as well as creating costly operations.
So based on my rules above, I KNOW, that I have rules in place to allow logged in users to read the Games node and that ALL users can read the Version node even non authenticated users.
As far as I know, it needs to be this way because I require ALL logged in users to be able to access the Games node information, else how would they be able to view the list of games they can select from!?
As for the Version node, I use that in the instance I need everyone that downloaded my app to "Force Upgrade" my app cause of a change that is required. In this instance, I would need the user that have downloaded an older version of my app and that are either "logged in" or "not logged in" and force them to update the app or else they can not use it.
Can anyone let me know if I am off base with how I structured my security rules or is this "normal" and that I am receiving the email just as a FYI!?
How have others setup their rules!? or what are the "best practices" for setting up security rules!? esp. if you need logged in users to access the information of any particular node(s) 24/7!?
The main problem with your current rules is that anyone can sign in through the API and read your entire database, even if they know nothing about your application. They can just read the root of the database, and then start looking at your data.
The first step to improve security would be to not allow read on the top-level, but only at lower levels:
{
"rules": {
".write": false,
"Games": {
".read" : "auth != null",
".indexOn" : "WeekId"
},
"Version": {
".read" : true
}
...
}
Now nobody can read from the root, and you must know that Games or Version node exists in order to read it.
I got my app working with read and write with wide-open permissions and now I'm locking it down. My app won't read or write though. I get permission denied errors despite the Firebase rules simulator saying that my rules are ok for a Facebook authenticated user whose UID I got from a successful firebase signInWithProvider. What am I missing?
{
"rules": {
"items": {
"$uid": {
// user must match the authenticated user
".read": "auth.uid == $uid",
".write": "auth.uid == $uid"
}
}
}
}
This is my data structure:
my-firebase-app
-items: {
-<uid123> : [
{label:'apple'},
{label:'banana'}
]
-<uid456> : [
{label:'pear'},
{label:'cherry'}
]
}
I sign into firebase after facebook auth by doing, firestack.auth.signInWithProvider(provider, facebookAccessToken, ''), which gives me my user object including uid
I push to /items/uid123 and get an item id, 333
I set the item {label:'apple'} for the new ID (333) at /items/uid123/333
I subscribe to the collection of items at /items/uid123 by doing this with the web sdk: itemsRef.child(uid).on('value', (snapshot) => .....
My set call looks like,
const newPostRef = itemsRef.child(uid).push();
newPostRef.set(itemWithID)
set promise gets rejected with permission denied error.
All the things above work fine if my .read and .write are simply set to true which leads me to think my syntax or structure is just off in the rules def. Would love some input.
There is nothing wrong with the security rules, and in theory, both the set and the query operations look fine.
In practice however, you are making a grave mistake. As you revealed in the comments, you are trying to use both the Web SDK and the react-native-firestack library at the same time!
The authentication state is not shared between the two, thus the on('value') query is completely unauthenticated. If you added the third parameter to on (the cancel callback), you would see the permission denied error.
You must eliminate the web SDK completely, and use the realtime database via firestack too.
I'm developing a proof-of-concept application, and thus my security requirements are low. I therefore changed the access rules to public access:
{
"rules": {
".read": true,
".write": true
}
}
This works as expected. The problem is that after a few minutes, the rules change back to default values (auth required) and I get an access error "Error: permission_denied at /cases: Client doesn't have permission to access the desired data."
Why on earth is this so?
Thanks.
You must edit database.rules.json in your IDE then save it then deploy it.
I built a simple landing page with Angular and offer visitors who may be interested in the service to send their data through a standard sign-up form. The data are stored in a firebase database.
What would be the recommended approach to best protect my database knowing that:
my firebase url is public (currently stored in the javascript)
the page is public (so anyone can 'write' to the database)
So far I've added the following rules to my firebase console:
{
rules: {
".read": "auth != null", // nobody can read
visitors: {
".write": "!data.exists()", // nobody can modify existing data
"firstname": {".validate": "newData.isString() && newData.val().length < 40"}, // only string with less than 40 characters
"lastname": {".validate": "newData.isString() && newData.val().length < 40"}, // only string with less than 40 characters
"email": {}, // no rules
"message": {".validate": "newData.isString() && newData.val().length < 500"} // only string with less than 500 characters
}
}
}
Question 1: Are these rules good enough to protect my database or should I consider adding additional ones?
Question 2: Should I consider hiding my Firebase URL on a backend server? My idea would be to put the firebase url in a php file on the server; To update the database with new visitor data, the app would first make an ajax call to my server through the $http service, get the firebase url back, to then update the firebase database. In that case the firebase URL would not be public anymore.
What do you think? Many thanks
These rules validate the format of the data that can be written. They in no way limit who can write this data. Whether these validation rules are sufficient for your security requirements, only you can tell.
Some developers wrap the Firebase Database API with their own server. But most developers expose the database directly to the users of their app and then put their server-side logic behind the database, as described in this page and diagram:
Those developers typically end up with stricter security rules than you have now though.
Note: This question is tagged polymer because the Polymer library is used to generate the Javascript.
This question is about two different but related problems dealing with Firebase security. Problem 1 and Problem 2 seem to suggest opposite realities and opposite results when trying to get the Firebase security rules to work. (FWIW: Here is my prior unsuccessful attempt to write this question.)
Here is the live code in a JSBin.
http://jsbin.com/hinehoyigo/edit?html,output
Problem 1
Firebase Security Rules Block Writing to Firebase
Instructions
Enter your firebase ID. Example: "hot-sauce-123"
In a new tab or window, go to your firebase and open it
Allow anonymous auth:
Your firebase
> Login & Auth
> Anonymous tab
> Check "Enable Anonymous User Authentication"
Apply no security rules: Security & Rules >
{
"rules": {
".read": true,
".write": true
}
}
Return back to jsbin
Select "anonymous" as auth provider: dropdown menu > anonymous
Click button labeled "login"
Verify login status per fields to right
Open your console: Chrome > View > Developer > Developer Tools
Click button labeled "Print User to console"
Double-check login status by verifying user object printed to console
Click button labeled "Print Firebase to console"
Verify correct firebase by checking Firebase URL printed to console
Click button labeled "Write to Firebase — Plain Url"
Check your firebase data; notice no write occured
Click button labeled "Write to Firebase — .json Url"
Check your firebase data; notice successful write
Distinguish the two write attempts because former attempts to write {"quux":"baz"};
latter attempts to write {"jquux":"jbaz"}
Add security rules: Security & Rules >
{
"rules": {
"users": {
"$uid": {
".read": "auth != null && auth.uid === $uid",
".write": "auth != null && auth.uid === $uid"
}
}
}
}
Click again the button labeled "Write to Firebase — .json Url"
Check your firebase data; notice write NOW FAILS
Conclusion / Problem Statement: Firebase security rules block writing to Firebase.
Problem 2
Simulator blocks .json URL from being used (as required above)
Instructions
Open your Firebase
Verify security rules are in place (see above step #19 in Problem #1)...
Open your simulator: Vertical Navigation Menu > Simulator
Check radio button labeled "Custom Auth"
Custom Auth input field should read something like
"{ provider: 'anonymous', uid: 'ag69g401-f097-4171-bca4-927fd1a6e1f3' }"
Click button labeled "Authenticate"
Verify little green check mark is next to radio button labeled "Custom Auth"
Go to section 2
Click tab labeled "Write"
In input field labeled URL, enter "users/ag69g401-f097-4171-bca4-927fd1a6e1f3/foo/bar"
Verify the user ID in the URL path you just entered, matches the uid value in the above Custom Auth field
Click button labeled "Simulate Write"
Notice response says something like:
Attempt to write {"key":"value"} to /users/ag69g401-f097-4171-bca4-927fd1a6e1f3/foo/bar with auth={"provider":"anonymous","uid":"ag69g401-f097-4171-bca4-927fd1a6e1f3"}
/
/users
/users/ag69g401-f097-4171-bca4-927fd1a6e1f3:.write: "auth != null && auth.uid === $uid"
=> true
Write was allowed.
Notice: This result is the opposite result from above steps #14 and #15 where the plain URL (without .json appended) failed to write
Append ".json" to the URL so the input field reads something like
"users/ag69g401-f097-4171-bca4-927fd1a6e1f3/foo/bar.json"
Click button labeled "Simulate Write"
Notice response says something like:
Invalid path. Simulation aborted.
Notice: This result is the opposite result from above steps #16 and #17 where the .json appended URL was the only one that worked;
Notice these simulator tests suggest the opposite results than those tested from the live code in above Problem #1
Problem 1
You're using the Polymer firebase-auth element to authenticate with Firebase.
<firebase-auth id="firebaseLogin"
user="{{user}}"
...
Under the hood this calls the Firebase.authAnonymously() method, which authenticates the current session in the Firebase JavaScript SDK.
You're writing to Firebase using:
computeFbTargetJson: function(f) {
return f + '.json';
}
and
this.set('$.ajax.url' , this.fbTargetJson);
this.set('$.ajax.body' , JSON.stringify({
jquux: 'jbaz'
}));
this.$.ajax.generateRequest();
This is doing an AJAX request to Firebase, which means that you're using Firebase's REST API. But since this is a regular HTTP call, it will not "inherit" then authentication from the Firebase JavaScript client.
To authenticate your REST request, you will need to pass in an auth parameter as documented in the Firebase documentation. You can get the token from the Firebase reference using ref.getAuth().token.
Problem 2
Each element in your Firebase database is accessibly by a unique URL. You can either access it with one of the Firebase SDKs or by directly accessing the REST API that Firebase exposes.
From what I can gather from the jsbin, you're writing to Firebase using:
computeFbTargetJson: function(f) {
return f + '.json';
}
and
this.set('$.ajax.url' , this.fbTargetJson);
this.set('$.ajax.body' , JSON.stringify({
jquux: 'jbaz'
}));
this.$.ajax.generateRequest();
This is doing an AJAX request to Firebase, which means that you're using Firebase's REST API. The first paragraph of that API documentation explains how Firebase database URLs map to the REST API:
We can use any Firebase app URL as a REST endpoint. All we need to do is append .json to the end of the URL and send a request from our favorite HTTPS client.
So in order to access any data in Firebase using the REST API, you append .json to the URL of that data.
When you're using the Firebase Simulator, the operation is similar to using Firebase's JavaScript SDK. In that case, you should not put the .json suffix at the end of the URL.
So given a Firebase database at https://mine.firebaseio.com/, say you have a user profile at /users/de01fc8104.
You can access that record in the simulator or with a Firebase SDK as https://mine.firebaseio.com/users/de01fc8104
To access the record using the REST API, the URL is https://mine.firebaseio.com/users/de01fc8104.json
Summary
Both of your problems are caused by the fact that you're using two different ways of accessing Firebase: the JavaScript SDK and the REST API. While these two methods can be used together, you will have to ensure you provide the information that is needed. In general you will have an easier experience if you stick to using one way of accessing Firebase from a single client.
#FrankvanPuffelen's answer in Polymer-speak translates into:
Use the <firebase-collection> element's .add() method to write data to Firebase from the client while under security rule enforcement. It's more efficient from a code standpoint as firebase-collection handles all the necessary auth tokens for you automatically.