34

I'm Using FirebaseSimpleLogin to create users and handle authentication.

When I try and create a new user with simple login via the $createUser() method, firebase won't create the user if the email address has already been used. However, I am also using $set() to save my created users to my firebase after I create them and I am using user.uid as the key. When trying to write to the database, firebase will save the record even if the username is not unique since only email and password are required for simple login. So how can I validate a username to be unique when it is not being used as the key to the user object?

I'm creating new users like this:

$scope.createUser = function() {
  $scope.auth.$createUser('trinker@gmail.com', 'password').then(function(user, err) {
    if (!err) {
      ref.child('users/' + user.uid).set({
        email: user.email,
        username: user.username
      });
      console.log("success!");
    }else{
      console.log(err.message);
    }
  });
}

And my user object looks like this:

{
  "users" : {
    "simplelogin:28" : {
      "email" : "trinker@gmail.com",
      "username" : "jtrinker"
    },
    "simplelogin:30" : {
      "email" : "test@gmail.com",
      "username" : "jtrinker"
    }
  }
}

}

I need to use the uid as the key for each user, but I still need the username to be unique.

How can I can I prevent firebase from saving records if properties within one object are not unique to properties inside a different object?

Nik
  • 709
  • 4
  • 22
reknirt
  • 2,237
  • 5
  • 29
  • 48
  • 2
    Why would you write the user record if they did not successfully create an account? Why would unauthenticated users be writing records? Could you elaborate on why this is a good idea? – Kato Aug 13 '14 at 22:25
  • 1
    I guess I wasn't clear. Obviously unauthenticated users writing records is a very bad idea, as you so explicitly imply. That's not the point, however. The point is, since a username isn't required to create a user account, how do I make sure that there are no duplicate user names in my firebase? – reknirt Aug 13 '14 at 23:39
  • You question asks how to make the email address unique. If the email address is already taken, they shouldn't be able to create the account, and therefore should not be able to authenticate, which would preclude creating the user account for a duplicate email address. So I'm still not quite grasping the larger use case we're trying to solve. – Kato Aug 14 '14 at 00:02
  • I'm sorry for the confusion. At the end of my question I mentioned I also need users to have a username, which will also need to be unique. Yes, all email addresses will be unique since they wont be able to create an account if the emails already taken, however, a username isn't required for firebase simple login, so someone would be able to create a new user with a unique email address yet enter a duplicate username. I want to add a username key to my user object above, but I need to make sure that you can't write to users unless the username is also unique within all users in my firebase. – reknirt Aug 14 '14 at 00:09
  • I edited my question to hopefully be more clear. – reknirt Aug 14 '14 at 15:12

2 Answers2

35

First of all, if users already have a username, it's unique, and this is not going to go away, I'd recommend that you give up on using simple login uids. This is going to create nothing but issues trying to flip back and forth between the two, as you've already discovered here. Investigate creating your own tokens with a tool like firebase-passport-login and then store the records by username.

But since that wasn't your question, let's resolve that while we're here, since you may want to go ahead and enter the thorny briar of dual identities through which I have passed many times.

To make the username unique, store an index of usernames.

/users/$userid/username/$username
/usernames/$username/$userid

To ensure they are unique, add a security rule as follows on the user id in usernames/ path, which ensures only one user can be assigned per username and that the value is the user's id:

".write": "newData.val() === auth.uid && !data.exists()"

Now enforce that they match by adding the following to the username in the users/ record:

"users": {
   "$userid": {
      "username": {
         ".validate": "root.child('usernames/'+newData.val()).val() === $userid"
      }
   }
}

This will ensure the ids are unique. Be careful with read privileges. You may want to avoid those entirely since you don't want anyone looking up private emails or usernames. Something like I demonstrated in support for saving these would be ideal.

The idea here is that you try to assign the username and email, if they fail, then they already exist and belong to another user. Otherwise, you insert them into the user record and now have users indexed by uid and email.

To comply with SO protocol, here's the code from that gist, which is better read via the link:

var fb = new Firebase(URL);

function escapeEmail(email) {
   return email.replace('.', ',');
}

function claimEmail(userId, email, next) {
   fb.child('email_lookup').child(escapeEmail(email)).set(userId, function(err) {
      if( err ) { throw new Error('email already taken'); }
      next();
   });
}

function claimUsername(userId, username, next) {
   fb.child('username_lookup').child(username).set(userId, function(err) {
      if( err ) { throw new Error('username already taken'); }
      next();
   });   
}

function createUser(userId, data) {
   claimEmail(userId, data.email, claimUsername.bind(null, userId, data.username, function() {
      fb.child('users').child(userId).set(data);
   );   
}

And the rules:

{
  "rules": {
     "users": {
        "$user": {
           "username": {
               ".validate": "root.child('username_lookup/'+newData.val()).val() === auth.uid"
           },
           "email": {
               ".validate": "root.child('email_lookup').child(newData.val().replace('.', ',')).val() === auth.uid"
           }
        }
     },

     "email_lookup": {
        "$email": {
           // not readable, cannot get a list of emails!
           // can only write if this email is not already in the db
           ".write": "!data.exists()",
           // can only write my own uid into this index
           ".validate": "newData.val() === auth.uid"
        }
     },
     "username_lookup": {
        "$username": {
           // not readable, cannot get a list of usernames!
           // can only write if this username is not already in the db
           ".write": "!data.exists()",

           // can only write my own uid into this index
           ".validate": "newData.val() === auth.uid"
        }
     },
  }
}
Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
Kato
  • 40,352
  • 6
  • 119
  • 149
  • Thanks very much for the answer. Two questions: 1) What are the `email_lookup` and `username_lookup` objects referring to? I haven't seen that before. Is it in any documentation? And 2) If you went with the custom login with the firebase-passport you would use `username` as the key, rather than the id? Thanks! – reknirt Aug 15 '14 at 18:42
  • email_lookup and username_lookup would simply be a hash containing username -> userid and email -> userid. – Kato Aug 15 '14 at 19:21
  • I guess more specifically my question is what are `email_lookup` and `username_lookup`? They're not data like the rest of the items under `"rules:" {`. Do they act as functions? – reknirt Aug 18 '14 at 14:20
  • In the example I gave, they are data (I call these an index). They are keyed by the email/username and have a value of the simple login uid. They are written by the claimEmail() and claimUsername() methods. – Kato Aug 18 '14 at 21:16
  • i'm receiving No such method/property 'replace'. while the rule have newData.replace. Is that available in security rules? – Douglas Correa Oct 10 '14 at 15:07
  • A quick check of the docs can [confirm or deny](https://www.firebase.com/docs/security/api/string/replace.html) its existence. In this case, it's just a typo; should be newData.val().replace() (it works on strings, remember) – Kato Oct 10 '14 at 16:16
  • 6
    The only problem I see here is that it allows users to claim more than one username/email - they can't use several at a time, but they prevent other users from using them. But it seems like there's really no better solution without using custom tokens. – Gustavo Bicalho May 07 '15 at 22:24
  • Amazing answer Kato, I was wondering how you would clear out the old usernames? An admin cloud function that removes them periodically? What would be the check for the delete condition? Also, In javascript I have a call to update the /users and /username_lookup, is there a way to batch them into a transaction? – Nikos Jul 04 '20 at 17:37
  • @GustavoBicalho did you find a good way to do this? In particular, using the js api. – Nikos Jul 04 '20 at 17:39
  • this was so good I made a video about the rules https://www.youtube.com/watch?v=QgtlpccCaMo – Nikos Jul 04 '20 at 19:08
2

Wouldn't it be easier to just use the security rules to check for it's existence? I have mine set up as follows:

"usernames": {
  "$usernameid": {
    ".read": "auth != null",
        ".write": "auth != null  && (!data.exists() || !newData.exists())"
    }
    }

This allows the write if the username doesn't exist. I believe I got this directly from the Firebase docs.

Wayne Filkins
  • 317
  • 1
  • 5
  • 14