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?
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"
}
},
}
}
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.
Related
I have a page that is calling addCheckin() method which is inside a controller. In the controller, I am trying to create a reference as follows:
var ref = firebase.database().ref("users/" + $scope.whichuser + "/meetings/" +$scope.whichmeeting + "/checkins");
$scope.whichuser and $scope.whichmeeting are the $routeParams that I am passing from another route.
Here's my checkin controller-
myApp.controller("CheckinsController",
['$scope','$rootScope','$firebaseArray','$routeParams','$firebaseObject',
function($scope,$rootScope,$firebaseArray,$routeParams,$firebaseObject){
$scope.whichuser = $routeParams.uid;
$scope.whichmeeting = $routeParams.mid;
var ref = firebase.database().ref("users/" + $scope.whichuser + "/meetings/" +$scope.whichmeeting + "/checkins");
$scope.addCheckin = function(){
var checkinInfo = $firebaseArray(ref);
var data={
firstname:$scope.firstname,
lastname:$scope.lastname,
email:$scope.email,
date:firebase.database.ServerValue.TIMESTAMP
}
checkinInfo.$add(data);
}
}]);/*controller*/
There are two errors that I am getting here-
Error 1:
Error: permission_denied at /users/Vp2P1MqKm7ckXqV2Uy3OzTnn6bB3/meetings: Client doesn't have permission to access the desired data.
Error 2:
Error: permission_denied at /users/Vp2P1MqKm7ckXqV2Uy3OzTnn6bB3/meetings/-KT5tqMYKXsFssmcRLm6/checkins: Client doesn't have permission to access the desired data.
And this is what I am tring to achieve-
Go to Firebase console of your app
Select Database From Side Menu --> Select Rule From tabs above --> Update your rule like this
{
"rules": {
".read": true,
".write": true
}
}
hope it solve your problem . thanks :)
Firebase project by default starts with Firestore as database.
This issue can happen if the application uses "Firebase Realtime Database" and permission for it are not configured. Read and write permission for Firebase Realtime Database should be explicitly granted.
To do so, in Firebase console, Database > Pick "Realtime Database" instead of "Firestore Beta" from the dropdown beside Database > Rules > Set
{
/* Visit https://firebase.google.com/docs/database/security to learn more about security rules. */
"rules": {
".read": true,
".write": true
}
}
Hope that help!
As all provided answers include a security issue where everyone could write / delete entries in your database, which for instance could cause extensive costs and or complete loss of data, when used in a bad way.
Of course you need to use firebase authentication features to use those rules, but preventing write access for anonymous should be default. The following rule provides read access to everyone while keeping security.
{
"rules": {
".read": true,
".write": "auth.uid != null"
}
}
None of these answers provide the most secure way to set up the Realtime Database.
You don't want anyone to randomly access or write to your database
Even if the user is authenticated, you don't want them to access other's data
This rule should address all the cases:
{
"rules": {
"users": {
"$uid": {
".read": "$uid === auth.uid",
".write": "$uid === auth.uid"
}
}
}
}
Please register your self in firebase by using the below code
Firebase.auth()
.createUserWithEmailAndPassword(email, password)
.then((data) => console.log(data))
.catch(error => console.log(error))
and authenticate your self using registered email and password by below code
Firebase.auth()
.signInWithEmailAndPassword(email, password)
.then((data) => console.log(data))
.catch(error => console.log(error))
With the below rules in your real time database
{
"rules": {
".read": "auth != null",
".write": "auth != null"
}
}
Then automatically you will be able to access the data from the real time database
var ref = firebase.database().ref("users");
ref.on("value", function(snapshot) {
snapshot.forEach(function(childSnapshot) {
var childData = childSnapshot.val();
var id=childData.id;
console.log(childData);
});
});
While logging out add the below command to logout of the app
Firebase.auth().signOut()
In order to grant access to all authenticated users, go to database rules tab in firebase web console and copy/paste following rules:
{
"rules": {
".read": "auth != null",
".write": "auth != null"
}
}
It appears that the /users/{uid} routes can be written to but they can not be read from. I changed /users to /userx and it immediately began working.
I hope this can help you.
{
".read": true,
".write": "auth !== null && root.child('users').child(auth.uid).child('meetings').val() === true"
}
You can remove the string && root.child('users').child(auth.uid).child('meetings').val() === true"
and the result is the same.
You can also specify the time, till that you wanna allow like this:
Select Database From Side Menu --> Select Rule From tabs above --> Update your rule like this
{
"rules": {
".read": "now < 1672384350000", // 2022-12-30
".write": "now < 1672384350000", // 2022-12-30
}
}
If someone still get the Error: permission_denied after allowing correct read rights and are fetching data with some kind of firebase npm package; it could be because you're trying to read from Realtime Database instead of the Cloud Firestore.
I was using react-redux-firebase npm package for a quick and easy setup. By default it uses the Realtime Database. If you're using Cloud Firestore you need to state that in the config with useFirestoreForProfile set to true.
import { ReactReduxFirebaseProvider } from 'react-redux-firebase';
const store = configureStore();
const rrfProps = {
firebase,
config: {
userProfile: 'users',
useFirestoreForProfile: true // Firestore for Profile instead of Realtime DB
},
dispatch: store.dispatch,
createFirestoreInstance,
};
Probably similar issues with other packages for firebase, like flamelink that support Realtime Database but not Cloud Firestore as stated in this issue.
I'm trying to get a user by the username, when I use Meteor.users.findOne, it always return the current user. And if I user Meteor.users.find, it returns all current user document, and the profile.firstName and profile.lastName of the right matched username.
Meteor.publish('userByUsername', function(username) {
return Meteor.users.findOne({
username: username
}, {
fields: {
'profile.firstName': 1,
'profile.lastName': 1,
}
});
});
How can I get only the user that match with the username?
I think what you want is not publish, but a method access to particular username. Publish/Subscribe is great for datasets that often change - say posts on stackoverflow, facebook feed, news articles, etc.
You are looking to get first/last name of a particular user, this does not really change. So what you actually want is to create a server method that returns the first/last name of the user. And you can call this method from the client to access this data.
if (Meteor.isClient) {
//get username var
Meteor.call('findUser', username, function(err, res) {
console.log(res.profile.firstName + " " + res.profile.lastName);
});
}
if (Meteor.isServer) {
Meteor.methods({
findUser: function(username) {
return Meteor.users.findOne({
username: username
}, {
fields: {
'profile.firstName': 1,
'profile.lastName': 1
}
});
}
});
}
Notice that the client Meteor.call has a callback method. DB queries on Meteor server is asynchronous & non-blocking, so you need to access the result via a javascript callback function.
findOne finds and returns the first document that matches the selector. Publish method needs to return a cursor, you need to use find, instead of findOne:
Meteor.publish('userByUsername', function(username) {
return Meteor.users.find({
username: username
}, {
fields: {
'profile.firstName': 1,
'profile.lastName': 1,
}
});
});
Then you can call subscribe on the client:
Meteor.subscribe('userByUsername', 'bob');
And call Meteor.users.findOne({ username: 'bob' }); in your helper for example.
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.
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?
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);
}
});
};
Inside my web-app, Firebase always tells me permission denied while things are working fine in the simulator.
FIREBASE WARNING: set at /users/simplelogin:14 failed: permission_denied
I'm having trouble adding metadata (such as a name) when users register. data is simply a JSON object that contains a name property.
var signup = function (email, password, data) {
firebaseAuth
.$createUser(email, password)
.then(function (user) {
dataRef.child('users').child(user.uid)
.set(data);
}, function (err) {
console.error(err);
});
};
My firebase rules:
{
"rules": {
"users": {
"$user": {
".read": "$user == auth.uid",
".write": "$user == auth.uid"
}
}
}
}
Simulator works fine:
Attempt to write {"name":"Test"} to /users/simplelogin:7 with auth={"uid":"simplelogin:7"}
/
/users
/users/simplelogin:7:.write: "$user == auth.uid"
=> true
Write was allowed.
As #Kato pointed out you need to log the user in yourself after registration before you can do anything.
Firebase createUser docs
I'm using Firebase's Simple Login through AngularFire with the anonymous login method and I was wondering if there's anyway to set the displayName property of the user object returned from
$scope.loginObj.$login('anonymous').then(function(user) {
user.displayName // This is an empty string
}
I would like to set displayName and save it back in Firebase so that users can be shown as that displayName property. As far as I know you don't actually write anything to Simple Login yourself. It seems like the only time it's written to is if you're using something like email/password authentication and using $scope.loginObj.$createUser('name', 'password')
This isn't possible in the way you describe, however, you can do this to get the same behavior.
$scope.loginObj.$login('anonymous').then(function(user) {
if (!user) return;
$scope.userRef = (new Firebase('<Your Firebase>.firebaseio.com/users/')).child(user.uid);
$scope.userRef.child('displayName').on('value', function (snapshot) {
user.displayName = shapshot.val();
});
});
// Then elsewhere in your code, set the display name and user.displayName will be updated automatically
$scope.userRef.child('displayName').set("DISPLAY NAME");
You can even back this up with simple Security Rules:
{"rules":
"users": {
"$uid" {
".read": true,
".write": "$uid == auth.uid'
}
}
}
This ensures that only a correctly authenticated user can modify their own display name.