Enforcing unique usernames with Firebase simplelogin

The question:

I have recently followed a tutorial over on Thinkster for creating a web app using Angular and Firebase.

The tutorial uses the Firebase simpleLogin method allows a ‘profile’ to be created that includes a username.

Factory:

app.factory('Auth', function($firebaseSimpleLogin, $firebase, FIREBASE_URL, $rootScope) {
var ref = new Firebase(FIREBASE_URL);


var auth = $firebaseSimpleLogin(ref);

var Auth = {
    register: function(user) {
       return auth.$createUser(user.email, user.password);
    },
    createProfile: function(user) {
        var profile = {
            username: user.username,
            md5_hash: user.md5_hash
        };

        var profileRef = $firebase(ref.child('profile'));
        return profileRef.$set(user.uid, profile);
    },
    login: function(user) {
        return auth.$login('password', user);
    },
    logout: function() {
        auth.$logout();
    },
    resolveUser: function() {
        return auth.$getCurrentUser();
    },
    signedIn: function() {
        return !!Auth.user.provider;
    },
    user: {}
};

$rootScope.$on('$firebaseSimpleLogin:login', function(e, user) {
    angular.copy(user, Auth.user);
    Auth.user.profile = $firebase(ref.child('profile').child(Auth.user.uid)).$asObject();

    console.log(Auth.user);
});
$rootScope.$on('$firebaseSimpleLogin:logout', function() {
    console.log('logged out');

    if (Auth.user && Auth.user.profile) {
        Auth.user.profile.$destroy();
    }
    angular.copy({}, Auth.user);
});

return Auth;
});

Controller:

$scope.register = function() {
    Auth.register($scope.user).then(function(user) {
        return Auth.login($scope.user).then(function() {
            user.username = $scope.user.username;
            return Auth.createProfile(user);
        }).then(function() {
            $location.path('/');
        });
    }, function(error) {
        $scope.error = error.toString();
    });
};

At the very end of the tutorial there is a ‘next steps’ section which includes:

Enforce username uniqueness– this one is tricky, check out Firebase priorities and see if you can use them to query user profiles by username

I have searched and searched but can’t find a clear explanation of how to do this, particularly in terms of the setPriority() function of Firebase

I’m quite the Firebase newbie so any help here would be gratefully recieved.

There are a few similar questions, but I can’t seem to get my head around how to sort this out.

Enormous thanks in advance.


EDIT

From Marein’s answer I have updated the register function in my controller to:

$scope.register = function() {

    var ref = new Firebase(FIREBASE_URL);
    var q = ref.child('profile').orderByChild('username').equalTo($scope.user.username);
    q.once('value', function(snapshot) {
        if (snapshot.val() === null) {
            Auth.register($scope.user).then(function(user) {
                return Auth.login($scope.user).then(function() {
                    user.username = $scope.user.username;
                    return Auth.createProfile(user);
                }).then(function() {
                    $location.path('/');
                });
            }, function(error) {
                $scope.error = error.toString();
            });
        } else {
            // username already exists, ask user for a different name
        }
    });
};

But it is throwing an ‘undefined is not a function’ error in the line var q = ref.child('profile').orderByChild('username').equalTo($scope.user.username);. I have commented out the code after and tried just console.log(q) but still no joy.

EDIT 2

The issue with the above was that the Thinkster tutorial uses Firebase 0.8 and orderByChild is available only in later versions. Updated and Marein’s answer is perfect.

The Solutions:

Below are the methods you can try. The first solution is probably the best. Try others if the first one doesn’t work. Senior developers aren’t just copying/pasting – they read the methods carefully & apply them wisely to each case.

Method 1

There are two things to do here, a client-side check and a server-side rule.

At the client side, you want to check whether the username already exists, so that you can tell the user that their input is invalid, before sending it to the server. Where exactly you implement this up to you, but the code would look something like this:

var ref = new Firebase('https://YourFirebase.firebaseio.com');
var q = ref.child('profiles').orderByChild('username').equalTo(newUsername);
q.once('value', function(snapshot) {
  if (snapshot.val() === null) {
    // username does not yet exist, go ahead and add new user
  } else {
    // username already exists, ask user for a different name
  }
});

You can use this to check before writing to the server. However, what if a user is malicious and decides to use the JS console to write to the server anyway? To prevent this you need server-side security.

I tried to come up with an example solution but I ran into a problem. Hopefully someone more knowledgeable will come along. My problem is as follows. Let’s say your database structure looks like this:

{
  "profiles" : {
    "profile1" : {
      "username" : "Nick",
      "md5_hash" : "..."
    },
    "profile2" : {
      "username" : "Marein",
      "md5_hash" : "..."
    }
  }
}

When adding a new profile, you’d want to have a rule ensuring that no profile object with the same username property exists. However, as far as I know the Firebase security language does not support this, with this data structure.

A solution would be to change the datastructure to use username as the key for each profile (instead of profile1, profile2, …). That way there can only ever be one object with that username, automatically. Database structure would be:

{
  "profiles" : {
    "Nick" : {
      "md5_hash" : "..."
    },
    "Marein" : {
      "md5_hash" : "..."
    }
  }
}

This might be a viable solution in this case. However, what if not only the username, but for example also the email has to be unique? They can’t both be the object key (unless we use string concatenation…).

One more thing that comes to mind is to, in addition to the list of profiles, keep a separate list of usernames and a separate list of emails as well. Then those can be used easily in security rules to check whether the given username and email already exist. The rules would look something like this:

{
  "rules" : {
    ".write" : true,
    ".read" : true,
    "profiles" : {
      "$profile" : {
        "username" : {
          ".validate" : "!root.child('usernames').child(newData.val()).exists()"
        }
      }
    },
    "usernames" : {
      "$username" : {
        ".validate" : "newData.isString()"
      }
    }
  }
}

However now we run into another problem; how to ensure that when a new profile is created, the username (and email) are also placed into these new lists? [1]

This in turn can be solved by taking the profile creation code out of the client and placing it on a server instead. The client would then need to ask the server to create a new profile, and the server would ensure that all the necessary tasks are executed.

However, it seems we have gone very far down a hole to answer this question. Perhaps I have overlooked something and things are simpler than they seem. Any thoughts are appreciated.

Also, apologies if this answer is more like a question than an answer, I’m new to SO and not sure yet what is appropriate as an answer.

[1] Although maybe you could argue that this does not need to be ensured, as a malicious user would only harm themselves by not claiming their unique identity?

Method 2

I had a similar problem. But it was after registering the user with password and email. In the user profile could save a user name that must be unique and I have found a solution, maybe this can serve you.

Query for username unique in Firebase

        var ref = new Firebase(FIREBASE_URL + '/users');

        ref.orderByChild("username").equalTo(profile.username).on("child_added", function(snapshot) {
            if (currentUser != snapshot.key()) {
                scope.used = true;
            }   
        }); 

        ref.orderByChild("username").equalTo(profile.username).once("value", function(snap) {
            //console.log("initial data loaded!", Object.keys(snap.val()).length === count);
            if (scope.used) {
                console.log('username already exists');
                scope.used = false;
            }else{
                console.log('username doesnt exists, update it');
                userRef.child('username').set(profile.username);
            }   
        }); 
    };  


All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Comment