Introduction
I have this structure on my db
C- usernames
D- paola
-> userId: 7384-aaL732-8923dsnio92202-peesK
D- alex
-> userId: ...
D- adam
-> userId: ...
C- users
D- userId of paola
-> username: "paola"
-> ...
D- userId of alex
-> username: "alex"
-> ...
D- userId of adam
-> username: "adam"
-> ...
I am signing up users in the client side so I have had to write some security rules...
In my client code I do:
Add the username (document id) with the userId (document data) to the usernames collection
Create a user document in the users collection with the username and other stuff.
Security Rules
So, my security rules look like this:
function isUsernameOwner(username) {
return get(/databases/$(database)/documents/usernames/$(username)).data.userId == request.auth.uid;
}
match /users/{userId} {
// Every people can read the users collection (might be in the sign in form)
allow read: if true;
// As the users creation is made in the client side, we have to make sure
// it meets these requirements
allow write: if isSignedIn() &&
isSameUser(userId) &&
request.resource.data.keys().hasOnly(['email', 'username', 'name', 'birthday']) &&
isValidUsername(request.resource.data.username) &&
isUsernameOwner(request.resource.data.username); // <------- If I remove this all works fine
}
Problem
When I try to sign up, I get "Missing or insufficent permissions"... I think the problem is in the function isUsernameOwner() but I don't know what am I doing wrong... Am I accessing incorrectly the field userId in the username document? If not, is it possible that the batched write doesn't happen sequentially?
Pd: The signup process is made using a batched write (first write the username, then the user)
UPDATE
This is the javascript code in which I make the batched write:
// Firebase.js
createUser = (email, password, username, name, birthday) => {
return this.auth
.createUserWithEmailAndPassword(email, password)
.then((currentUser) => {
// Get the user id
const userId = currentUser.user.uid;
// Get a new Firestore batched write
const batch = this.db.batch();
// Create the new username document in the usernames collection with the user's id as field
const usernameRef = this.db.collection("usernames").doc(username);
batch.set(usernameRef, { userId });
// Create the new user document in the users collection with all the relevant information
const userRef = this.db.collection("users").doc(userId);
birthday = firebase.firestore.Timestamp.fromDate(new Date(birthday)); // It is neccessary to convert the birthday to a valid Firebase Timestamp
const data = {
email,
username,
name,
birthday,
};
batch.set(userRef, data);
// Commit the batch
return batch.commit();
})
.catch((err) => {
throw err;
});
I think the problem is that you are using get() in your security rule global function. Make it local and use getAfter instead to wait until the 'termination' of the batched write.
Here you can see a post which might be useful for your case: Firebase security rules difference between get() and getAfter()
Just see the Doug answer, he explains the differences between get and getAfer.
Related
This question already has answers here:
Firebase kicks out current user
(19 answers)
Closed 1 year ago.
I want to make a system where the administrator can create user auth from an email. I have developed as the documentation says but the current session is closed. I only want to create the auth to get the uid and then create a user in the database with the data I want to store.
This is what I have:
var email = emailInput.value;
var password = "Abcd1234";
firebase.auth().createUserWithEmailAndPassword(email, password).then((userCredential) => {
var user = userCredential.user;
//user.uid contains the id I want to create the instance on ref usuarios
database.ref("usuarios/"+ user.uid).set({...});
});
Edit:
You cannot create new users using client SDK. By that I mean a user creating new users as required. You need to use Firebase Admin SDK (which must be in a secure server environment - like Firebase Cloud Functions).
You can write a cloud function like this:
exports.createNewUser = functions.https.onCall((data, context) => {
if (isAdmin(context.auth.uid)) {
return admin.auth().createUser({
email: data.email,
password: data.password,
displayName: data.name
}).then((userRecord) => {
// See the UserRecord reference doc for the contents of userRecord.
console.log('Successfully created new user:', userRecord.uid);
return { uid: userRecord.uid }
}).catch((error) => {
console.log('Error creating new user:', error);
return { error: "Something went wrong" }
});
}
return {error: "unauthorized" }
})
Now there are multiple ways you could verify that the user who is calling this function is an admin. First one would be using Firebase Custom Claims which are somewhat like roles you assign to users. Another option would be storing UID of using in database and checking the UID exists in admin node of db. Just make sure only you can edit that part of the database.
To call the function from client:
const createNewUser = firebase.functions().httpsCallable('createNewUser');
createNewUser({ name: "test", email: "test#test.test", password: "122345678" })
.then((result) => {
// Read result of the Cloud Function.
var response = result.data;
});
Problem
I'd like to add several user data to firestore Authenticateced user list AND to users collection which I created by myself at same time, but it does't go well. users collection are updated only its part of it.
Data
// javascript
users: [
{email: "n_0#example.com", username: "user0"},
{email: "n_1#example.com", username: "user1"},
{email: "n_2#example.com", username: "user2"},
{email: "n_3#example.com", username: "user3"},
{email: "n_4#example.com", username: "user4"}
]
Code
// javascript
import * as app from 'firebase/app'
import 'firebase/auth'
const config = JSON.parse(process.env.VUE_APP_FIREBASE_CONFIG)
app.initializeApp(config)
export const firebase = app
export const auth = app.auth()
function asyncCreateUser(user) {
return auth.createUserWithEmailAndPassword(
user.email,
'password'
).then(function (createdUser) {
console.log('---')
console.log('user.email', user.email)
console.log('createdUser', createdUser.user.email)
const ref = usersRef.doc(createdUser.user.uid)
return ref.set(user)
})
}
this.users.map(user => asyncCreateUser(user))
Result
Authenticated users are ok.
users collection has problem: it has only three users in the collection. The number of users added to user collection may differ in different execution.
Log
Debug.vue?2083:50 user.email n_3#example.com
Debug.vue?2083:51 createdUser n_2#example.com
Debug.vue?2083:49 ---
Debug.vue?2083:50 user.email n_2#example.com
Debug.vue?2083:51 createdUser n_1#example.com
Debug.vue?2083:49 ---
Debug.vue?2083:50 user.email n_1#example.com
Debug.vue?2083:51 createdUser n_1#example.com
Debug.vue?2083:49 ---
Debug.vue?2083:50 user.email n_0#example.com
Debug.vue?2083:51 createdUser n_0#example.com
Debug.vue?2083:49 ---
Debug.vue?2083:50 user.email n_4#example.com
Debug.vue?2083:51 createdUser n_4#example.com
It's strange that in some section, user.email and createdUser are diffrent.
Help wanted
I'd like to know how to fix it. If possible, I'd like to know the causes too. Thank you!
Just a guess - you are using the javascript SDK to create the user, not the admin SDK.
The Javascript SDK logs the user in after creating the account, so basically you rapidly logged a new account in and out 5 times in a row, hence the mix up with the user ids when creating the firestore documents - it can be that you were just logged out at that moment:
If the new account was created, the user is signed in automatically.
Have a look at the Next steps section below to get the signed in user
details.
Firebase docs
If you want to bulk-create user accounts you are better off using the admin SDK in a secure environment (e.g. cloud functions) and simply trigger the https function from your frontend. The way you are doing it now means that all the accounts will be created sequentially which can be quite time consuming when you create lots at once - if you are using a cloud function and the admin sdk you can kick off the creation of all accounts in parallel and return a promise once all are finished - something along the lines of:
return Promise.all(users.map(user => admin.auth().createUser(user)
.then(function(userRecord) {
return admin.firestore().collection('users').doc(userRecord.uid).set(...);
})
})
Firebase admin reference
I solved it by my self. It seems async call in map made something wrong.
self = this
for (let i = 0; i < 5; i++) {
const self = this
await auth.createUserWithEmailAndPassword(data[i].email, 'password')
.then(function (res) {
const ref = usersRef.doc(res.user.uid)
self.data[i]._id = res.user.uid
ref.set(self.data[i])
}
)
}
I have implemented a Firebase SignUp with username, email and password. Basically what I am doing is:
1- Create user with email and password (if the username and email are not used by other users)
2- Add the username to the user
Like this:
firebase
.createUserWithEmailAndPassword(email, password)
.then((currentUser) => {
// Get the username from the input
const username = this.usernameInput.current.getText();
// Create a user with this username and the current user uid in the db
firebase.createUserWithUsername(username, currentUser.user.uid); // <----------
})
.catch((err) => {
// ...
});
And my createUserWithUsername function basically do this:
createUserWithUsername = (username, userId) => {
/*
Create a document in the usernames collection
which uid (of the document itself, not a field) is the given username.
*/
// Pass username to lowercase
username = username.toLowerCase();
// Initial user's data
const data = {
email: this.auth.currentUser.email,
username,
};
return this.db
.collection("usernames")
.doc(username)
.set({ userId })
.then(() => {
this.db.collection("users").doc(userId).set(data);
})
.catch((err) => {
console.log(err);
throw err;
});
/*
Pd: As firestore automatically removes empty documents, we have to
assign them a field. The user's id is a good option, because it will help us to
update usernames faster, acting like a 'Foreign Key' in a NoSql DB.
*/
};
My question is? Is it wrong to leave this code on the client side? Can it be a security problem? Do I need to move this to a cloud function / backend?
This is my firestore security rule for the usernames collection:
match /usernames/{username} {
function isUsernameAvailable() {
return !exists(/databases/$(database)/documents/usernames/$(username));
}
allow read: if true;
allow write, update: if isSignedIn() && isUsernameAvailable();
// TODO - Allow delete?
}
I would really appreciate any guide for this. Thank you.
This question already has answers here:
Cloud Firestore: Enforcing Unique User Names
(8 answers)
Closed 4 years ago.
I have a simple signup form where a user can set their details, including a username that needs to be unique.
I have written a rule to validate if the username already exists (which works) but even if the signup fails, the user account has been created.
Example signup script (stripped back):
try {
// This creates a user on submit of the form.
const data = await fb.auth.createUserWithEmailAndPassword(this.email, this.password)
// Use the uid we get back to create a user document in the users collection.
await db.collection('users').doc(data.user.uid).set({
username: this.username, // This fails in the rule if it exists.
firstName: this.firstName,
lastName: this.lastName
})
} catch (error) {
console.log(error)
}
The call to create a user document fails because the username is not unique (which is expected), but at this point in the flow, the user has already been created in Firebase!
If they then choose another username, they can't continue because Firestore already sees a user with that same email.
Is there a better way to create this flow?
Ideally, I do not want to create a user at all if the creation of the user document fails in some way.
Thanks!
Possible solution:
I was thinking I could just immediately delete the user after they're created if the try/catch block fails:
await data.user.delete() // ...but this seems hacky?
I would recommend using Cloud Functions here probably using an http onCall one would make it nice and simple. I've not tested the below but should get you almost there.
Clientside code
const createUser = firebase.functions().httpsCallable('createUser');
createUser({
email: this.email,
password: this.password,
username: this.username,
firstName: this.firstName,
lastName: this.lastName
}).then(function(result) {
console.log(result); // Result from the function
if (result.data.result === 'success') {
await firebase.auth().signInWithEmailAndPassword(this.email, this.password);
} else {
console.log('Username already exists')
}
});
Cloud function
exports.createUser = functions.https.onCall(async (data, context) => {
const email = data.email;
const password = data.password;
const username = data.username;
const firstName = data.firstName;
const lastName = data.lastName;
const usersQuery = await admin.firestore().collection('users').where('username', '==', username).get();
if (usersQuery.size > 0) {
return {
result: 'username-exists'
}
} else {
const user = await admin.auth().createUser({
displayName: username,
email: email,
password: password
});
await admin.firestore().collection('users').doc(user.uid).set({
username: username,
firstName: firstName,
lastName: lastName
});
return {
result: 'success'
}
}
});
If you want a certain value to be unique, consider using it as the document ID in a collection and disallow updates to that collection.
For example, since you want user names to be unique, create a collection usernames, where the ID of each document is the user name, and the content is the UID of the user who is using that name.
Also see:
Cloud Firestore: Enforcing Unique User Names
Firebase Firestore Unique Constraints
How to Enforce Unique Field Values in Cloud Firestore
I am wondering how to make a document for each user as they create their account (with Firebase Web). I have Firebase Authentication enabled and working, and I'd like each user then to have a document in Cloud Firestore in a collection named users. How would I get the UID and then automatically create a document for each user? (I am doing this so that calendar events can be saved into an array field in the document, but I need a document for the user to start with). I am aware and know how to make security rules for access, I just don't know how to make the document in the first place.
Thanks!
While it is definitely possible to create a user profile document through Cloud Functions, as Renaud and guillefd suggest, also consider creating the document directly from your application code. The approach is fairly similar, e.g. if you're using email+password sign-in:
firebase.auth().createUserWithEmailAndPassword(email, password)
.then(function(user) {
// get user data from the auth trigger
const userUid = user.uid; // The UID of the user.
const email = user.email; // The email of the user.
const displayName = user.displayName; // The display name of the user.
// set account doc
const account = {
useruid: userUid,
calendarEvents: []
}
firebase.firestore().collection('accounts').doc(userUid).set(account);
})
.catch(function(error) {
// Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
// ...
});
Aside from running directly from the web app, this code also creates the document with the user's UID as the key, which makes subsequent lookups a bit simpler.
You´ll have to set a firebase function triggered by the onCreate() Auth trigger.
1. create the function trigger
2. get the user created data
3. set the account data.
4. add the account data to the collection.
functions/index.js
// Firebase function
exports.createAccountDocument = functions.auth.user().onCreate((user) => {
// get user data from the auth trigger
const userUid = user.uid; // The UID of the user.
//const email = user.email; // The email of the user.
//const displayName = user.displayName; // The display name of the user.
// set account doc
const account = {
useruid: userUid,
calendarEvents: []
}
// write new doc to collection
return admin.firestore().collection('accounts').add(account);
});
If you are using Firebase UI to simplify your life a lil, you can add a User document to a "/users" collection in Firestore only when that user first signs up by using authResult.additionalUserInfo.isNewUser from the signInSuccessWithAuthResult in your UI config.
I'm doing something like this in my project:
let uiConfig = {
...
callbacks: {
signInSuccessWithAuthResult: (authResult) => {
// this is a new user, add them to the firestore users collection!
if (authResult.additionalUserInfo.isNewUser) {
db.collection("users")
.doc(authResult.user.uid)
.set({
displayName: authResult.user.displayName,
photoURL: authResult.user.photoURL,
createdAt: firebase.firestore.FieldValue.serverTimestamp(),
})
.then(() => {
console.log("User document successfully written!");
})
.catch((error) => {
console.error("Error writing user document: ", error);
});
}
return false;
},
},
...
}
...
ui.start("#firebaseui-auth-container", uiConfig);
The signInSuccessWithAuthResult gives you an authResult and a redirectUrl.
from the Firebase UI Web Github README:
// ...
signInSuccessWithAuthResult: function(authResult, redirectUrl) {
// If a user signed in with email link, ?showPromo=1234 can be obtained from
// window.location.href.
// ...
return false;
}