2

I've been stuck on this problem for many hours now, so any help is appreciated. Trying to make an Android app with Firebase for user authentication (simple-login email and password) along with unique usernames. I have my sign-up screen fields laid out on a single screen e.g.:

"Enter Username"
"Enter Email Address"
"Enter Password"

I'm very confused as how how to query the database to check if the username exists or not without reading a database snapshot and without attempting to write the username to the database (because this is happening while the user is in state auth == null, so while on the sign-up page before the user has created his account I want to inform the user whether his username is taken or not).

I mean, this feels like it should be very simple, just a simple query to Firebase with a string and just getting Firebase to return True or False, but after hours of googling I could find nothing.

The reason I don't want to use a snapshot to do this is because I do not want to expose all my user's names and their UIDs to the public by setting "read" to true (I followed this guide so my security rules are set up just like this, along with my database structure Firebase android : make username unique).

Here are my rules, and they work currently (I don't like the fact that the read is set to True which is why I'm asking the question though):

{
  "rules": {
    "usernames": {
      ".read": true,
      "$username": {
        ".write": "auth !== null && !data.exists()"
      }
    },
    "users": {
        "$uid": {
            ".write": "auth !== null && auth.uid === $uid && !data.exists()",
            ".read": "auth !== null && auth.provider === 'password' && auth.uid === $uid",
            "username": {
              ".validate": "(!root.child('users').child(newData.val()).exists() || root.child('usernames').child(newData.val()).val() == $uid)"
     }
    }
   }
 }
}

And this is my data:

{
  "usernames" : {
    "abcd" : "some-user-uid"
  },
  "users" : {
    "\"some-user-uid\"" : {
      "username" : "abcd"
    }
  }
}

Thanks!

Community
  • 1
  • 1
Aretyper
  • 53
  • 5

1 Answers1

1

There is unfortunately, no way to test whether the data exists without actually downloading it via the SDK. Data structures are going to be supreme here (recommended reading: NoSQL Data Structures and you shouldn't be afraid to denormalize a bit of data when optimization and scale are critical.

Generally speaking, you should keep your data well structured so payloads are small and fetch it. If you're fetching something that can't wait for the bytes to be fetched (e.g. games, strange one-off admin ops on very large data sets, et al) then here are a few reasonable approaches to simulate this:

Fetching a list of keys via the REST API

Using the attribute shallow=true in a call to the REST API will prevent loading of a large data set and return only the keys at that path. Note that if you store a million records, they still have to be loaded into memory on the server (slow) and you still have to fetch a million strings (expensive).

So one way to check the existence of data at a path, without actually downloading the data, would be to make a call to the path, such as https://<YOUR-FIREBASE-APP>.firebaseio.com/foo.json?shallow=true, and check whether any keys are returned.

Creating a denormalized index you can query instead

If you really need to squeeze some extra performance and speed out of your Firebase Database (hint: you don't need this unless you're running millions of queries per minute and probably only for gaming logic and similar), you can dual-write your records (i.e. denormalize) as follows:

/foo/data/$id/... data goes here...
/foo/index/$id/true (just a boolean value)

To dual write, you would use the update command, and a write similar to the following (Android SDK sample):

public void addRecord(Map<String, Object> data) {
   DatabaseReference db = FirebaseDatabase.getInstance().getReference();

   // create a new record id (a key)
   String key = db.child("foo").push().getKey();

   // construct the update map
   Map<String, Object> dualUpdates = new HashMap<>();
   dualUpdates.put("/data/" + key, /* data here */);
   dualUpdates.put("/index/" + key, true);

   // save the new record and the index at the same time
   db.child("foo").updateChildren(dualUpdates);
}

Now to determine if a record exists, without actually downloading the data, I can simply query against /foo/index/$id and try DataSnapshot.exists(), at the cost of downloading a single boolean.

Kato
  • 40,352
  • 6
  • 119
  • 149
  • Thanks for the answer! What solution do you suggest for maximum security of the data? I'm alright with my current setup (because it works) but the read = true is slightly unnerving because then anyone can see all my users' usernames and their UIDs, correct? – Aretyper Jan 31 '17 at 00:30
  • The best security would be to set read: true on the specific records, but not allow read of the parent path. That would prevent anyone from browsing the uids; they would need to know a user's id to look them up. I don't know what your use case is here, so I can't really offer any specific advice. But you may want to read up on the [XY problem](http://meta.stackexchange.com/questions/66377/what-is-the-xy-problem/66378#66378), as you may be asking the wrong questions. – Kato Jan 31 '17 at 15:43