0

I have carefully looked at other answers to similar questions and as far as I can tell everything is set correctly but still access is denied.

Minimum working example:

My Firebase Live Database Security Rule for the path 'user/{uid}' is as follows

"rules": {
          "user": {
                    "$uid": {
                              ".read":     "auth.uid === $uid",
                              ".write":    "auth.uid === $uid && !data.exists() "
                            }
                  }
       }

in the typescript I attempt to read 'user/{uid}' for some user.

//Firebase and firebase-fcuntions import
const firebase       = require("firebase");
const functions      = require('firebase-functions');

//Reference to root of database
const rootdb = firebase.database();

//Read data
function foo(data, context){
    return rootdb.ref('user').child(context.auth.uid).once('value')
    .then(snap => //do stuff)
    .catch( err => { console.log("Unsuccessful.")})
 }

//Make call available from application using authentication
exports.enable_foo = functions.https.onCall( (data, context)  => foo(data, context)  );

The logs on firebase display:

Error: permission_denied at /user/XCRR0JK3xxZMoyoKzTIeQ2n1HcY2: Client doesn't have permission to access the desired data. a...

and the "Unsuccesful" message for the catch path of execution prints.

What am I missing?

Edit 1

I should mention that the actual method, as opposed to the minimum working example above, does check for the user being logged in and prints the user auth.uid so I can confirm the user is logged in.

//Firebase and firebase-fcuntions import
const firebase       = require("firebase");
const functions      = require('firebase-functions');

//Reference to root of database
const rootdb = firebase.database();

//Read data
function foo(data, context){


    // Checking that the user is authenticated.
    if (!context.auth) {
        console.log("No authentication.")
        throw new functions.https.HttpsError('Authentication', "You are not authorized to execute this function." );
    }

    console.log(  context.auth.uid )
    return rootdb.ref('user').child(context.auth.uid).once('value')
    .then(snap => //do stuff)
    .catch( err => { console.log("Unsuccessful.")})
 }

//Make call available from application using authentication
exports.enable_foo = functions.https.onCall( (data, context)  => foo(data, context)  );

When I execute this function the {uid} of the user shows up in the logs when I print it.

Edit 2

Replacing the 'firebase' requirement by "firebase-admin" appears to "fix" the issue, that is to say it allows the read.

I have a security concern with this, namely that users who are authenticated and DO have access to a resource are denied said resource if I use the "firebase" requirement. Needing the full access "firebase-admin" to allow a user to access(read or write) a resource seems over kill and unintended.

So, I suppose the question now is: is this intended behaviour?

2 Answers2

1

According to the Firebase documentation

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"
      }
    }
  }
}

These rules give anyone, even people who are not users of your app, read and write access to your database.

{  
  "rules": {
    ".read": true,
    ".write": true
  }
}

These rules don't allow anyone read or write access to your database.

{  
  "rules": {
    ".read": false,
    ".write": false
  }
}

Make sure that your code is reading the security rules for the chosen database, such as Cloud Firestore database or Firebase Realtime Database, which are totally distinct databases and use different security rules to control access. Also, check if your user is first authenticated before fetching data from the DB.

sllopis
  • 2,292
  • 1
  • 8
  • 13
  • I added more information to the question. I am already doing both of those things. I also changed the rule to ".read": true for a moment and that does allow the read, so I am not sure why the read is denied. – ReverseFlowControl Sep 11 '19 at 13:41
  • If you tried seting ".read": true for your "rules", then it means that anyone, even people who are not users of your app, read and write access to your database. Therefore I agree with @Frank van Puffelen on the fact that the error log is rarely caused by the code you provided. Are you using any official tutorial or guidance to do this? – sllopis Sep 12 '19 at 11:51
  • I am not. That is the only code I have. I changed the rule back to the authenticated version. I changed the firebase variable to use require("firebase-admin") instead. Now it works and I can access the path "user/{uid}", but now I am concerned that just any user could access a path and not just the intended user. – ReverseFlowControl Sep 12 '19 at 15:27
  • Glad it works. In order to prevent any random person to have access to the mentioned path, then you should try the rule that grants access to a node matching the authenticated user's ID from the Firebase auth token that I shared earlier in my post. For more info about rules, visit the website I provided also. Keep my posted. – sllopis Sep 12 '19 at 15:37
1

When your client app invokes a callable function, the identity of the authenticated user is passed along to the function, but that doesn't mean the function is somehow automatically initializing any other modules with the user's credentials. So, if you import the Firebase client SDK, it is effectively running in unauthenticated mode (if it works at all - it was meant to run in browser environments, not nodejs).

Since backend code runs in a privileged environment that the user can't modify, it's typically said to be safe to run any code, since you wrote it, and you know exactly what it does. That's why it's recommended to user firebase-admin for access, which is meant primarily for backends and not client apps.

Now, if your concern is that the user might trigger the function and try to make it do something on their behalf that they shouldn't be able to do, you will have to write some code for that. The typical choice is to validate that the UID is allowed to do what the function is going to do. This means duplicating the checks of any security rules that would normally be used to protect the database.

Your other choice, which is going to suffer in performance, is to use the databaseAuthVariableOverride to tell the Admin SDK that it should use a different UID to access Realtime Database. Then it will respect all security rules. The problem here is that you have to init and then delete the firebase-admin instance for each request so that you don't leak memory. I show an example of this in another answer that uses an HTTP function to receive an auth ID token, validate its UID, and use the UID to init the SDK. Since you are using a callable type function, the validation has already been done for you, so all you have to do is use the given UID from the function in the same way.

Doug Stevenson
  • 297,357
  • 32
  • 422
  • 441