Using Firebase Security rules, how can I check for available usernames without making the entire node public?

Viewed 123

In my Firebase database, I have a section for storing usernames that are taken. There is a “usernames” node, where the username is the key, and the user’s ID is stored in a “userId” atrribute.

usernames
{
    username1
        userId : "exampleId1"
    username2
        userId : "exampleId2"
    username3
        userId : "exampleId3"
    ...
}

When a user is signing up, before they create an account and are Authenticated, the app must check that the username is not taken.

In order for this to work, the “usernames” node has been set to public in the Firebase Security Rules:

"usernames": {
  ".read": true
}

Unfortunately, this will make every taken username and internal user ID visible, which is a security concern and not something that should be done.

(for those that don’t know, public nodes can be accessed through a browser like so):

https://mydatabasename.firebaseio.com/usernames.json

There are other nodes for banned usernames and emails that work in a similar way; they have to be checked before a user is Authenticated, and should not be fully exposed to the public.

My question is: When a user is signing up, how can I check for available usernames without making the entire node public?

3 Answers

To know if a specific user name is already taken, the user doesn't need read permission to /usernames but it's suffice to give them read access to /usernames/$username. So:

"usernames": {
  "$username": {
    ".read": true
  }
}

With these rules, you code can check whether the specific user name that the user wants to claim is already taken (by someone else), but they can't request a list of all user names.

Two options comes to mind, the first is allowing the public read access to your database while the second method is what I would do in a real project:

Method 1: Maintain a Separate "Usernames" Node

With this method you create a secondary node, let's say it's called usernamesInUse and this would be world readable. Its structure would look like this:

{
  "usernamesInUse": {
    "username1": true,
    "username2": true,
    "username3": true
  }
}

Checking if a username exists is as simple as:

db().ref('usernamesInUse/username2').once('value', (snapshot) => if (snapshot.exists()) ...)

The downsides to this method are that you have to have processes in place to update this node whenever a new user is added, modified or deleted. However this would give secure read access to usernames and nothing else.

Method 2: Create a Cloud Function (How I would do it)

Create a simple Cloud Function with an HTTPS endpoint that checks for the existence of the username and returns a 200 or 404 status code. Your database would not need any world readable permissions.

This avoids the need to duplicate data, prevents users from downloading a full list of every user in your system and prevents the world from unmetered access to your database. You also have the opportunity to block access to abusive anonymous visitors.

Tell me if you like the idea. :)

I would create a singleton with all func and variable private which will only return a Bool and take Username variable. This way no one can access data or func from this part. No injections possible.

Insisde the singleton you can check all usernames and do whatever you want. return only Bool.

Related